diff --git a/.deploy/api/Dockerfile b/.deploy/api/Dockerfile index cde2dd3ec39..10e7ac2b8dc 100644 --- a/.deploy/api/Dockerfile +++ b/.deploy/api/Dockerfile @@ -159,6 +159,8 @@ COPY --chown=node:node packages/plugins/sentry-tracing/package.json ./packages/p COPY --chown=node:node packages/plugins/product-reviews/package.json ./packages/plugins/product-reviews/ COPY --chown=node:node packages/plugins/knowledge-base/package.json ./packages/plugins/knowledge-base/ COPY --chown=node:node packages/plugins/changelog/package.json ./packages/plugins/changelog/ +COPY --chown=node:node packages/plugins/job-search/package.json ./packages/plugins/job-search/ +COPY --chown=node:node packages/plugins/job-proposal/package.json ./packages/plugins/job-proposal/ # We do not build here Wakatime plugin, because it used in Desktop Apps for now # COPY --chown=node:node packages/plugins/integration-wakatime/package.json ./packages/plugins/integration-wakatime/ @@ -215,6 +217,8 @@ COPY --chown=node:node packages/plugins/sentry-tracing/package.json ./packages/p COPY --chown=node:node packages/plugins/product-reviews/package.json ./packages/plugins/product-reviews/ COPY --chown=node:node packages/plugins/knowledge-base/package.json ./packages/plugins/knowledge-base/ COPY --chown=node:node packages/plugins/changelog/package.json ./packages/plugins/changelog/ +COPY --chown=node:node packages/plugins/job-search/package.json ./packages/plugins/job-search/ +COPY --chown=node:node packages/plugins/job-proposal/package.json ./packages/plugins/job-proposal/ # We do not build here Wakatime plugin, because it used in Desktop Apps for now # COPY --chown=node:node packages/plugins/integration-wakatime/package.json ./packages/plugins/integration-wakatime/ diff --git a/.deploy/webapp/Dockerfile b/.deploy/webapp/Dockerfile index 0a601166b1f..1577e409758 100644 --- a/.deploy/webapp/Dockerfile +++ b/.deploy/webapp/Dockerfile @@ -71,9 +71,11 @@ COPY --chown=node:node packages/plugins/integration-github/package.json ./packag COPY --chown=node:node packages/plugins/integration-jira/package.json ./packages/plugins/integration-jira/ COPY --chown=node:node packages/plugins/jitsu-analytics/package.json ./packages/plugins/jitsu-analytics/ COPY --chown=node:node packages/plugins/sentry-tracing/package.json ./packages/plugins/sentry-tracing/ +COPY --chown=node:node packages/plugins/job-search/package.json ./packages/plugins/job-search/ COPY --chown=node:node packages/plugins/product-reviews/package.json ./packages/plugins/product-reviews/ COPY --chown=node:node packages/plugins/knowledge-base/package.json ./packages/plugins/knowledge-base/ COPY --chown=node:node packages/plugins/changelog/package.json ./packages/plugins/changelog/ +COPY --chown=node:node packages/plugins/job-proposal/package.json ./packages/plugins/job-proposal/ # We do not build here Wakatime plugin, because it used in Desktop Apps for now # COPY --chown=node:node packages/plugins/integration-wakatime/package.json ./packages/plugins/integration-wakatime/ diff --git a/.env.compose b/.env.compose index d2a044ba155..cee27bd1e5e 100644 --- a/.env.compose +++ b/.env.compose @@ -79,7 +79,7 @@ DB_CA_CERT= REDIS_ENABLED=true # redis[s]://[[username][:password]@][host][:port][/db-number] -REDIS_URL=redis://localhost:6379 +REDIS_URL=redis://redis:6379 EXPRESS_SESSION_SECRET=gauzy diff --git a/.env.demo.compose b/.env.demo.compose index 78d973eb3f8..0a0e71f02b0 100644 --- a/.env.demo.compose +++ b/.env.demo.compose @@ -81,7 +81,7 @@ DB_CA_CERT= # we don't run Redis in basic Demo setup REDIS_ENABLED=false # redis[s]://[[username][:password]@][host][:port][/db-number] -REDIS_URL=redis://localhost:6379 +REDIS_URL=redis://redis:6379 EXPRESS_SESSION_SECRET=gauzy diff --git a/README.md b/README.md index 424881aa319..13c5566617f 100644 --- a/README.md +++ b/README.md @@ -185,26 +185,32 @@ Please refer to our official [Platform Documentation](https://docs.gauzy.co) and ### With Docker Compose - Clone repo. -- Make sure you have Docker Compose [installed locally](https://docs.docker.com/compose/install). -- Run `docker-compose up`, if you want to run the platform in production configuration using our prebuild Docker images. Check `.env.compose` file for different settings (optionally), e.g. DB type. _(Note: Docker Compose will use latest images pre-build automatically from head of `master` branch using GitHub CI/CD.)_ -- Run `docker-compose -f docker-compose.demo.yml up`, if you want to run the platform in basic configuration (e.g. for Demo / explore functionality / quick run) using our prebuild Docker images. Check `.env.demo.compose` file for different settings (optionally), e.g. DB type. _(Note: Docker Compose will use latest images pre-build automatically from head of `master` branch using GitHub CI/CD.)_ +- Make sure you have latest Docker Compose [installed locally](https://docs.docker.com/compose/install). Important: you need minimum [v2.20](https://docs.docker.com/compose/release-notes/#2200). +- Run `docker-compose -f docker-compose.demo.yml up`, if you want to run the platform in basic configuration (e.g. for Demo / explore functionality / quick run) using our prebuilt Docker images. Check `.env.demo.compose` file for different settings (optionally), e.g. DB type. _(Note: Docker Compose will use latest images pre-build automatically from head of `master` branch using GitHub CI/CD.)_ +- Run `docker-compose up`, if you want to run the platform in production configuration using our prebuilt Docker images. Check `.env.compose` file for different settings (optionally), e.g. DB type. _(Note: Docker Compose will use latest images pre-build automatically from head of `master` branch using GitHub CI/CD.)_ - Run `docker-compose -f docker-compose.build.yml up`, if you want to build everything (code and Docker images) locally. Check `.env.compose` file for different settings (optionally), e.g. DB type. _(Note: this is extremely long process because it builds whole platform locally. Other options above are much faster!)_ -- :coffee: time... It might take some time for our API to seed fake data in the DB during the first Docker Compose run, even if you used prebuild Docker images. +- :coffee: time... It might take some time for our API to seed fake data in the DB during the first Docker Compose run, even if you used prebuilt Docker images. - Open in your browser. - Login with email `admin@ever.co` and password: `admin` for Super Admin user. - Login with email `employee@ever.co` and password: `123456` for Employee user. - Enjoy! -Together with Gauzy, Docker Compose will run the following: +Notes: + +- while demo `docker-compose.demo.yml` runs minimum amount of containers (API, Web UI and DB), other Docker Compose files run multiple infrastructure dependencies (see full list below). +- you can also run ONLY infra dependencies (without our API / Web containers) with `docker-compose -f docker-compose.infra.yml up` command. We already doing it using `include` in our main docker compose files. + +Together with Gauzy, Docker Compose (i.e. `docker-compose.yml` and `docker-compose.build.yml`, not Demo `docker-compose.demo.yml`) will run the following: - [PostgreSQL](https://www.postgresql.org) - Primary Database. - [Pgweb](https://github.com/sosedoff/pgweb) - Cross-platform client for PostgreSQL DBs, available on . - [ElasticSearch](https://github.com/elastic/elasticsearch) - Search Engine. - [Dejavu](https://github.com/appbaseio/dejavu) - Web UI for ElasticSearch, available on . - [MinIO](https://github.com/minio/minio) - Multi-Cloud ☁️ Object Storage (AWS S3 compatible). -- [Jitsu](https://github.com/jitsucom/jitsu) - Jitsu is an open-source Segment alternative. Fully scriptable data ingestion engine for modern data teams. +- [Jitsu](https://github.com/jitsucom/jitsu) - Jitsu is an open-source Segment alternative (data ingestion engine). - [Redis](https://github.com/redis/redis) - In-memory data store/caching (also used by Jitsu) - [Cube](https://github.com/cube-js/cube) - "Semantic Layer" used for Reports, Dashboards, Analytics, and other BI-related features, with UI available on . +- [Zipkin](https://github.com/openzipkin/zipkin) - distributed tracing system. ### Manually @@ -256,7 +262,7 @@ Notes: - Another variant to deploy Gauzy is to use DigitalOcean Droplets or any other virtual instance (with Ubuntu OS) and deploy using SCP/SSH, for example, following [GitHub Action](https://github.com/ever-co/ever-gauzy/blob/develop/.github/workflows/deploy-do-droplet-demo.yml) -#### Pulumi +#### Pulumi - In addition, check [Gauzy Pulumi](https://github.com/ever-co/ever-gauzy-pulumi) project (WIP), it makes complex Clouds deployments possible with a single command (`pulumi up`). Note: it currently supports AWS EKS (Kubernetes) for development and production with Application Load Balancers and AWS RDS Serverless PostgreSQL DB deployments. We also implemented deployments to ECS EC2 and Fargate Clusters in the same Pulumi project. diff --git a/apps/api/package.json b/apps/api/package.json index 8e682227233..74e6cbdae90 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -40,10 +40,12 @@ "seed:prod:build": "yarn ng run api:seed -c=production" }, "dependencies": { - "@gauzy/core": "^0.1.0", "@gauzy/changelog-plugin": "^0.1.0", - "@gauzy/knowledge-base-plugin": "^0.1.0", + "@gauzy/core": "^0.1.0", "@gauzy/jitsu-analytics-plugin": "^0.1.0", + "@gauzy/job-proposal-plugin": "^0.1.0", + "@gauzy/job-search-plugin": "^0.1.0", + "@gauzy/knowledge-base-plugin": "^0.1.0", "@gauzy/sentry-plugin": "^0.1.0", "dotenv": "^16.0.3", "yargs": "^17.5.0" diff --git a/apps/api/src/migration.ts b/apps/api/src/migration.ts index 57108c181e1..e1d87f70f8b 100644 --- a/apps/api/src/migration.ts +++ b/apps/api/src/migration.ts @@ -61,4 +61,4 @@ yargs createMigration(pluginConfig, { name }); } }) - .argv; // To set above changes \ No newline at end of file + .argv; // To set above changes diff --git a/apps/api/src/plugins.ts b/apps/api/src/plugins.ts index 2caf7b7822a..7b574198fdb 100644 --- a/apps/api/src/plugins.ts +++ b/apps/api/src/plugins.ts @@ -2,6 +2,8 @@ import { environment } from '@gauzy/config'; import { ChangelogPlugin } from '@gauzy/changelog-plugin'; import { JitsuAnalyticsPlugin } from '@gauzy/jitsu-analytics-plugin'; import { KnowledgeBasePlugin } from '@gauzy/knowledge-base-plugin'; +import { JobSearchPlugin } from '@gauzy/job-search-plugin'; +import { JobProposalPlugin } from '@gauzy/job-proposal-plugin'; import { SentryTracing as SentryPlugin } from './sentry'; const { jitsu, sentry } = environment; @@ -10,10 +12,8 @@ const { jitsu, sentry } = environment; * An array of plugins to be included or used in the codebase. */ export const plugins = [ - // Indicates the inclusion or intention to use the ChangelogPlugin in the codebase. - ChangelogPlugin, - // Indicates the inclusion or intention to use the KnowledgeBasePlugin in the codebase. - KnowledgeBasePlugin, + // Includes the SentryPlugin based on the presence of Sentry configuration. + ...(sentry && sentry.dsn ? [SentryPlugin] : []), // Initializes the Jitsu Analytics Plugin by providing a configuration object. JitsuAnalyticsPlugin.init({ config: { @@ -23,6 +23,12 @@ export const plugins = [ echoEvents: jitsu.echoEvents } }), - // Includes the SentryPlugin based on the presence of Sentry configuration. - ...(sentry && sentry.dsn ? [SentryPlugin] : []), + // Indicates the inclusion or intention to use the ChangelogPlugin in the codebase. + ChangelogPlugin, + // Indicates the inclusion or intention to use the KnowledgeBasePlugin in the codebase. + KnowledgeBasePlugin, + // Indicates the inclusion or intention to use the JobProposalPlugin in the codebase. + JobProposalPlugin, + // Indicates the inclusion or intention to use the JobSearchPlugin in the codebase. + JobSearchPlugin ]; diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 24c4e80ef04..cdcc1a5dec7 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -37,7 +37,9 @@ "../../../packages/plugins/knowledge-base", "../../../packages/plugins/changelog", "../../../packages/plugins/jitsu-analytics", - "../../../packages/plugins/sentry-tracing" + "../../../packages/plugins/sentry-tracing", + "../../../packages/plugins/job-search", + "../../../packages/plugins/job-proposal" ] }, "build": { @@ -143,6 +145,8 @@ "@gauzy/desktop-libs": "^0.1.0", "@gauzy/desktop-window": "^0.1.0", "@gauzy/jitsu-analytics-plugin": "^0.1.0", + "@gauzy/job-proposal-plugin": "^0.1.0", + "@gauzy/job-search-plugin": "^0.1.0", "@gauzy/knowledge-base-plugin": "^0.1.0", "@gauzy/sentry-plugin": "^0.1.0", "@nestjs/platform-express": "^10.3.7", @@ -158,11 +162,14 @@ "electron-store": "^8.1.0", "electron-updater": "^6.1.7", "electron-util": "^0.17.2", + "embedded-queue": "^0.0.11", "ffi-napi": "^4.0.3", "form-data": "^3.0.0", "htmlparser2": "^8.0.2", "iconv": "^3.0.1", "knex": "^3.1.0", + "locutus": "^2.0.30", + "mac-screen-capture-permissions": "^2.1.0", "moment": "^2.29.4", "node-fetch": "^2.6.7", "node-notifier": "^8.0.0", @@ -170,16 +177,13 @@ "pdfmake": "^0.2.0", "pg-query-stream": "^4.5.4", "pg": "^8.11.4", + "screenshot-desktop": "^1.15.0", "sound-play": "1.1.0", "sqlite3": "^5.1.7", "squirrelly": "^8.0.8", "tslib": "^2.3.0", "twing": "^5.0.2", - "locutus": "^2.0.30", "underscore": "^1.13.3", - "screenshot-desktop": "^1.15.0", - "mac-screen-capture-permissions": "^2.1.0", - "embedded-queue": "^0.0.11", "undici": "^6.10.2" }, "optionalDependencies": { diff --git a/apps/gauzy-e2e/src/integration/AddTasksTest.ts b/apps/gauzy-e2e/src/integration/AddTasksTest.ts index e9109473cee..f8f7ba877ce 100644 --- a/apps/gauzy-e2e/src/integration/AddTasksTest.ts +++ b/apps/gauzy-e2e/src/integration/AddTasksTest.ts @@ -24,7 +24,7 @@ describe('Add tasks test', () => { lastName = faker.person.lastName(); username = faker.internet.userName(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/integration/AddUserTest.ts b/apps/gauzy-e2e/src/integration/AddUserTest.ts index 0f7cd4847ff..c7e4d62aa88 100644 --- a/apps/gauzy-e2e/src/integration/AddUserTest.ts +++ b/apps/gauzy-e2e/src/integration/AddUserTest.ts @@ -18,7 +18,7 @@ describe('Add user test', () => { firstName = faker.person.firstName(); lastName = faker.person.lastName(); username = faker.internet.userName(); - email = faker.internet.email(); + email = faker.internet.exampleEmail(); password = faker.internet.password(); imgUrl = faker.image.avatar(); diff --git a/apps/gauzy-e2e/src/integration/AppointmentsTest.ts b/apps/gauzy-e2e/src/integration/AppointmentsTest.ts index 16acd96ce16..164b6cef4ad 100644 --- a/apps/gauzy-e2e/src/integration/AppointmentsTest.ts +++ b/apps/gauzy-e2e/src/integration/AppointmentsTest.ts @@ -20,7 +20,7 @@ describe('Book public appointment test', () => { lastName = faker.person.lastName(); username = faker.internet.userName(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/integration/ApprovalRequestTest.ts b/apps/gauzy-e2e/src/integration/ApprovalRequestTest.ts index e6337f66d16..4d24ad953e8 100644 --- a/apps/gauzy-e2e/src/integration/ApprovalRequestTest.ts +++ b/apps/gauzy-e2e/src/integration/ApprovalRequestTest.ts @@ -22,7 +22,7 @@ describe('Approval request test', () => { lastName = faker.person.lastName(); username = faker.internet.userName(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/integration/CandidatesTest.ts b/apps/gauzy-e2e/src/integration/CandidatesTest.ts index 96349f7c386..376d4a56d78 100644 --- a/apps/gauzy-e2e/src/integration/CandidatesTest.ts +++ b/apps/gauzy-e2e/src/integration/CandidatesTest.ts @@ -17,8 +17,8 @@ let imgUrl = ' '; describe('Invite candidate test', () => { before(() => { - email = faker.internet.email(); - secondEmail = faker.internet.email(); + email = faker.internet.exampleEmail(); + secondEmail = faker.internet.exampleEmail(); firstName = faker.person.firstName(); lastName = faker.person.lastName(); username = faker.internet.userName(); diff --git a/apps/gauzy-e2e/src/integration/ClientsTest.ts b/apps/gauzy-e2e/src/integration/ClientsTest.ts index 66e45c75bc3..f8e76cf38e4 100644 --- a/apps/gauzy-e2e/src/integration/ClientsTest.ts +++ b/apps/gauzy-e2e/src/integration/ClientsTest.ts @@ -21,7 +21,7 @@ let website = ' '; describe('Clients test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); fullName = faker.person.firstName() + ' ' + faker.person.lastName(); inviteName = faker.person.firstName() + ' ' + faker.person.lastName(); deleteName = faker.person.firstName() + ' ' + faker.person.lastName(); diff --git a/apps/gauzy-e2e/src/integration/ContactsLeadsTest.ts b/apps/gauzy-e2e/src/integration/ContactsLeadsTest.ts index ed56fb4f72d..1aab91f7a54 100644 --- a/apps/gauzy-e2e/src/integration/ContactsLeadsTest.ts +++ b/apps/gauzy-e2e/src/integration/ContactsLeadsTest.ts @@ -20,7 +20,7 @@ let website = ' '; describe('Contacts leads test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); fullName = faker.person.firstName() + ' ' + faker.person.lastName(); deleteName = faker.person.firstName() + ' ' + faker.person.lastName(); city = faker.location.city(); diff --git a/apps/gauzy-e2e/src/integration/CustomersTest.ts b/apps/gauzy-e2e/src/integration/CustomersTest.ts index 70ac23f451b..dd732494927 100644 --- a/apps/gauzy-e2e/src/integration/CustomersTest.ts +++ b/apps/gauzy-e2e/src/integration/CustomersTest.ts @@ -20,7 +20,7 @@ let website = ' '; describe('Customers test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); fullName = faker.person.firstName() + ' ' + faker.person.lastName(); deleteName = faker.person.firstName() + ' ' + faker.person.lastName(); city = faker.location.city(); diff --git a/apps/gauzy-e2e/src/integration/EditEmployeeTest.ts b/apps/gauzy-e2e/src/integration/EditEmployeeTest.ts index e627fd5d060..79beb2ea28a 100644 --- a/apps/gauzy-e2e/src/integration/EditEmployeeTest.ts +++ b/apps/gauzy-e2e/src/integration/EditEmployeeTest.ts @@ -39,8 +39,8 @@ describe('Edit employee test', () => { lastName = faker.person.lastName(); username = faker.internet.userName(); password = faker.internet.password(); - email = faker.internet.email(); - employeeEmail = faker.internet.email(); + email = faker.internet.exampleEmail(); + employeeEmail = faker.internet.exampleEmail(); fullName = faker.person.firstName() + ' ' + faker.person.lastName(); imgUrl = faker.image.avatar(); city = faker.location.city(); @@ -49,7 +49,7 @@ describe('Edit employee test', () => { editUsername = faker.internet.userName(); editFirstName = faker.person.firstName(); editLastName = faker.person.lastName(); - editEmail = faker.internet.email(); + editEmail = faker.internet.exampleEmail(); contactCity = faker.location.city(); contactPostcode = faker.location.zipCode(); contactStreet = faker.location.streetAddress(); diff --git a/apps/gauzy-e2e/src/integration/EditUserTest.ts b/apps/gauzy-e2e/src/integration/EditUserTest.ts index 9190f5f0ef4..6cfb9525b21 100644 --- a/apps/gauzy-e2e/src/integration/EditUserTest.ts +++ b/apps/gauzy-e2e/src/integration/EditUserTest.ts @@ -22,7 +22,7 @@ describe('Edit user test', () => { firstName = faker.person.firstName(); lastName = faker.person.lastName(); username = faker.internet.userName(); - email = faker.internet.email(); + email = faker.internet.exampleEmail(); password = faker.internet.password(); imgUrl = faker.image.avatar(); editFirstName = faker.person.firstName(); diff --git a/apps/gauzy-e2e/src/integration/EstimatesTest.ts b/apps/gauzy-e2e/src/integration/EstimatesTest.ts index dce39f1f55b..5fc1cb22288 100644 --- a/apps/gauzy-e2e/src/integration/EstimatesTest.ts +++ b/apps/gauzy-e2e/src/integration/EstimatesTest.ts @@ -22,13 +22,13 @@ let sendEmail = ' '; describe('Estimates test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); fullName = faker.person.firstName() + ' ' + faker.person.lastName(); city = faker.location.city(); postcode = faker.location.zipCode(); street = faker.location.streetAddress(); website = faker.internet.url(); - sendEmail = faker.internet.email(); + sendEmail = faker.internet.exampleEmail(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); }); diff --git a/apps/gauzy-e2e/src/integration/GoalsKPITest.ts b/apps/gauzy-e2e/src/integration/GoalsKPITest.ts index 58af3853600..15aeb5f26a4 100644 --- a/apps/gauzy-e2e/src/integration/GoalsKPITest.ts +++ b/apps/gauzy-e2e/src/integration/GoalsKPITest.ts @@ -20,7 +20,7 @@ describe('Goals KPI test', () => { lastName = faker.person.lastName(); username = faker.internet.userName(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/integration/HumanResourcesTest.ts b/apps/gauzy-e2e/src/integration/HumanResourcesTest.ts index 30ecc7a73c1..e8ce1547568 100644 --- a/apps/gauzy-e2e/src/integration/HumanResourcesTest.ts +++ b/apps/gauzy-e2e/src/integration/HumanResourcesTest.ts @@ -20,7 +20,7 @@ describe('Human resources page test', () => { lastName = faker.person.lastName(); username = faker.internet.userName(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/integration/InviteUserTest.ts b/apps/gauzy-e2e/src/integration/InviteUserTest.ts index 93fadc11801..6ceaaa0c6b8 100644 --- a/apps/gauzy-e2e/src/integration/InviteUserTest.ts +++ b/apps/gauzy-e2e/src/integration/InviteUserTest.ts @@ -11,8 +11,8 @@ let secEmail = ' '; describe('Invite user/s test', () => { before(() => { - email = faker.internet.email(); - secEmail = faker.internet.email(); + email = faker.internet.exampleEmail(); + secEmail = faker.internet.exampleEmail(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); }); diff --git a/apps/gauzy-e2e/src/integration/InvoicesTest.ts b/apps/gauzy-e2e/src/integration/InvoicesTest.ts index 5113fc2200b..c6389f9ed34 100644 --- a/apps/gauzy-e2e/src/integration/InvoicesTest.ts +++ b/apps/gauzy-e2e/src/integration/InvoicesTest.ts @@ -22,13 +22,13 @@ let sendEmail = ' '; describe('Invoices test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); fullName = faker.person.firstName() + ' ' + faker.person.lastName(); city = faker.location.city(); postcode = faker.location.zipCode(); street = faker.location.streetAddress(); website = faker.internet.url(); - sendEmail = faker.internet.email(); + sendEmail = faker.internet.exampleEmail(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); }); diff --git a/apps/gauzy-e2e/src/integration/ManageCandidatesInvitesTest.ts b/apps/gauzy-e2e/src/integration/ManageCandidatesInvitesTest.ts index ef5e6c542e0..677df5cf2e9 100644 --- a/apps/gauzy-e2e/src/integration/ManageCandidatesInvitesTest.ts +++ b/apps/gauzy-e2e/src/integration/ManageCandidatesInvitesTest.ts @@ -9,7 +9,7 @@ let email = ' '; describe('Manage candidates invites test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); }); diff --git a/apps/gauzy-e2e/src/integration/ManageEmployeesTest.ts b/apps/gauzy-e2e/src/integration/ManageEmployeesTest.ts index 9640437c0ad..e026a9d9fa1 100644 --- a/apps/gauzy-e2e/src/integration/ManageEmployeesTest.ts +++ b/apps/gauzy-e2e/src/integration/ManageEmployeesTest.ts @@ -21,14 +21,14 @@ let imgUrl = ' '; describe('Manage employees test', () => { before(() => { - email = faker.internet.email(); - secEmail = faker.internet.email(); + email = faker.internet.exampleEmail(); + secEmail = faker.internet.exampleEmail(); firstName = faker.person.firstName(); lastName = faker.person.lastName(); username = faker.internet.userName(); - email = faker.internet.email(); + email = faker.internet.exampleEmail(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/integration/ManageInterviewsTest.ts b/apps/gauzy-e2e/src/integration/ManageInterviewsTest.ts index 4874346c2b5..d61979c24e9 100644 --- a/apps/gauzy-e2e/src/integration/ManageInterviewsTest.ts +++ b/apps/gauzy-e2e/src/integration/ManageInterviewsTest.ts @@ -16,7 +16,7 @@ let imgUrl = ' '; describe('Manage interviews test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); firstName = faker.person.firstName(); lastName = faker.person.lastName(); username = faker.internet.userName(); diff --git a/apps/gauzy-e2e/src/integration/OrganizationDepartmentsTest.ts b/apps/gauzy-e2e/src/integration/OrganizationDepartmentsTest.ts index db8fe94b3dc..c2599b2a172 100644 --- a/apps/gauzy-e2e/src/integration/OrganizationDepartmentsTest.ts +++ b/apps/gauzy-e2e/src/integration/OrganizationDepartmentsTest.ts @@ -22,14 +22,14 @@ let imgUrl = ' '; describe('Organization departments test', () => { before(() => { - email = faker.internet.email(); - secEmail = faker.internet.email(); + email = faker.internet.exampleEmail(); + secEmail = faker.internet.exampleEmail(); firstName = faker.person.firstName(); lastName = faker.person.lastName(); username = faker.internet.userName(); - email = faker.internet.email(); + email = faker.internet.exampleEmail(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/integration/ProposalsTest.ts b/apps/gauzy-e2e/src/integration/ProposalsTest.ts index 123da5977c6..ae388136a4f 100644 --- a/apps/gauzy-e2e/src/integration/ProposalsTest.ts +++ b/apps/gauzy-e2e/src/integration/ProposalsTest.ts @@ -29,7 +29,7 @@ describe('Proposals test', () => { lastName = faker.person.lastName(); username = faker.internet.userName(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/integration/RegisterTest.ts b/apps/gauzy-e2e/src/integration/RegisterTest.ts index d97fb92a9db..2ebfad9badf 100644 --- a/apps/gauzy-e2e/src/integration/RegisterTest.ts +++ b/apps/gauzy-e2e/src/integration/RegisterTest.ts @@ -17,7 +17,7 @@ let street = ' '; describe('Register Test', () => { before(() => { fullName = faker.person.fullName(); - email = faker.internet.email(); + email = faker.internet.exampleEmail(); pass = faker.internet.password(); organizationName = faker.company.name(); taxId = faker.string.alphanumeric(); diff --git a/apps/gauzy-e2e/src/integration/RemoveUserTest.ts b/apps/gauzy-e2e/src/integration/RemoveUserTest.ts index 6c91f79620f..1f2e3436a80 100644 --- a/apps/gauzy-e2e/src/integration/RemoveUserTest.ts +++ b/apps/gauzy-e2e/src/integration/RemoveUserTest.ts @@ -19,7 +19,7 @@ describe('Remove user test', () => { firstName = faker.person.firstName(); lastName = faker.person.lastName(); username = faker.internet.userName(); - email = faker.internet.email(); + email = faker.internet.exampleEmail(); password = faker.internet.password(); imgUrl = faker.image.avatar(); diff --git a/apps/gauzy-e2e/src/integration/SalesEstimatesTest.ts b/apps/gauzy-e2e/src/integration/SalesEstimatesTest.ts index c17604189d8..764c55592b7 100644 --- a/apps/gauzy-e2e/src/integration/SalesEstimatesTest.ts +++ b/apps/gauzy-e2e/src/integration/SalesEstimatesTest.ts @@ -22,13 +22,13 @@ let sendEmail = ' '; describe('Sales estimates test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); fullName = faker.person.firstName() + ' ' + faker.person.lastName(); city = faker.location.city(); postcode = faker.location.zipCode(); street = faker.location.streetAddress(); website = faker.internet.url(); - sendEmail = faker.internet.email(); + sendEmail = faker.internet.exampleEmail(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); }); diff --git a/apps/gauzy-e2e/src/integration/SalesInvoicesTest.ts b/apps/gauzy-e2e/src/integration/SalesInvoicesTest.ts index c685f814f19..02cf79f3810 100644 --- a/apps/gauzy-e2e/src/integration/SalesInvoicesTest.ts +++ b/apps/gauzy-e2e/src/integration/SalesInvoicesTest.ts @@ -22,13 +22,13 @@ let sendEmail = ' '; describe('Sales invoices test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); fullName = faker.person.firstName() + ' ' + faker.person.lastName(); city = faker.location.city(); postcode = faker.location.zipCode(); street = faker.location.streetAddress(); website = faker.internet.url(); - sendEmail = faker.internet.email(); + sendEmail = faker.internet.exampleEmail(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); }); diff --git a/apps/gauzy-e2e/src/integration/TimeOffTest.ts b/apps/gauzy-e2e/src/integration/TimeOffTest.ts index cf4a5606b24..9c58180ff04 100644 --- a/apps/gauzy-e2e/src/integration/TimeOffTest.ts +++ b/apps/gauzy-e2e/src/integration/TimeOffTest.ts @@ -20,7 +20,7 @@ describe('Time Off test', () => { lastName = faker.person.lastName(); username = faker.internet.userName(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/integration/TimesheetsTest.ts b/apps/gauzy-e2e/src/integration/TimesheetsTest.ts index 8a6061bf243..a81384c4f92 100644 --- a/apps/gauzy-e2e/src/integration/TimesheetsTest.ts +++ b/apps/gauzy-e2e/src/integration/TimesheetsTest.ts @@ -31,7 +31,7 @@ let website = ' '; describe('Timesheets test', () => { before(() => { - email = faker.internet.email(); + email = faker.internet.exampleEmail(); fullName = faker.person.firstName() + ' ' + faker.person.lastName(); city = faker.location.city(); postcode = faker.location.zipCode(); @@ -42,7 +42,7 @@ describe('Timesheets test', () => { lastName = faker.person.lastName(); username = faker.internet.userName(); password = faker.internet.password(); - employeeEmail = faker.internet.email(); + employeeEmail = faker.internet.exampleEmail(); imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); diff --git a/apps/gauzy-e2e/src/support/step_definitions/AddRemoveExistingUserTest/AddRemoveExistingUserTest.ts b/apps/gauzy-e2e/src/support/step_definitions/AddRemoveExistingUserTest/AddRemoveExistingUserTest.ts index 4a6b73a45ba..9c003574cfd 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/AddRemoveExistingUserTest/AddRemoveExistingUserTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/AddRemoveExistingUserTest/AddRemoveExistingUserTest.ts @@ -15,7 +15,7 @@ const pageLoadTimeout = Cypress.config('pageLoadTimeout'); let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let password = faker.internet.password(); let imgUrl = faker.image.avatar(); let editFirstName = faker.person.firstName(); diff --git a/apps/gauzy-e2e/src/support/step_definitions/AddTasksTest/AddTasksTest.ts b/apps/gauzy-e2e/src/support/step_definitions/AddTasksTest/AddTasksTest.ts index 402a96506b1..768e7f3cbe2 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/AddTasksTest/AddTasksTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/AddTasksTest/AddTasksTest.ts @@ -20,7 +20,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/AppointmentsTest/AppointmentsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/AppointmentsTest/AppointmentsTest.ts index a47f50050d1..e947a2f792f 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/AppointmentsTest/AppointmentsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/AppointmentsTest/AppointmentsTest.ts @@ -20,7 +20,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/ApprovalRequestTest/ApprovalRequestTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ApprovalRequestTest/ApprovalRequestTest.ts index 9b98e808c1e..ac34dc60b7a 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ApprovalRequestTest/ApprovalRequestTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ApprovalRequestTest/ApprovalRequestTest.ts @@ -18,7 +18,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); let defaultName = faker.person.jobTitle() + ' ' + ApprovalRequestPageData.defaultRequest; diff --git a/apps/gauzy-e2e/src/support/step_definitions/CandidatesTest/CandidatesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/CandidatesTest/CandidatesTest.ts index ff81195d27c..52454257bbe 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/CandidatesTest/CandidatesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/CandidatesTest/CandidatesTest.ts @@ -12,8 +12,8 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); -let secondEmail = faker.internet.email(); +let email = faker.internet.exampleEmail(); +let secondEmail = faker.internet.exampleEmail(); let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); diff --git a/apps/gauzy-e2e/src/support/step_definitions/ClientsTest/ClientsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ClientsTest/ClientsTest.ts index 9d694f91312..d5d5f83cbcb 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ClientsTest/ClientsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ClientsTest/ClientsTest.ts @@ -13,7 +13,7 @@ import * as logoutPage from '../../Base/pages/Logout.po'; import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let inviteName = faker.person.firstName() + ' ' + faker.person.lastName(); let deleteName = faker.person.firstName() + ' ' + faker.person.lastName(); diff --git a/apps/gauzy-e2e/src/support/step_definitions/ContactsLeadsTest/ContactsLeadsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ContactsLeadsTest/ContactsLeadsTest.ts index f757435e4c8..41a1c8c2d75 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ContactsLeadsTest/ContactsLeadsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ContactsLeadsTest/ContactsLeadsTest.ts @@ -14,7 +14,7 @@ import * as manageEmployeesPage from '../../Base/pages/ManageEmployees.po'; import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let deleteName = faker.person.firstName() + ' ' + faker.person.lastName(); let inviteName = faker.person.firstName() + ' ' + faker.person.lastName(); @@ -27,7 +27,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/CustomersTest/CustomersTest.ts b/apps/gauzy-e2e/src/support/step_definitions/CustomersTest/CustomersTest.ts index 182d541f2f3..d3a6c78b06b 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/CustomersTest/CustomersTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/CustomersTest/CustomersTest.ts @@ -16,7 +16,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let deleteName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); @@ -28,7 +28,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/EditProfileTest/EditProfileTest.ts b/apps/gauzy-e2e/src/support/step_definitions/EditProfileTest/EditProfileTest.ts index 3c7f6dea18d..bc85b2909aa 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/EditProfileTest/EditProfileTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/EditProfileTest/EditProfileTest.ts @@ -16,7 +16,7 @@ const pageLoadTimeout = Cypress.config('pageLoadTimeout'); let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let password = faker.internet.password(); let editFirstName = faker.person.firstName(); let editLastName = faker.person.lastName(); diff --git a/apps/gauzy-e2e/src/support/step_definitions/EmployeeAddInfoTest/EmployeeAddInfoTest.ts b/apps/gauzy-e2e/src/support/step_definitions/EmployeeAddInfoTest/EmployeeAddInfoTest.ts index 22070bd4aa3..7f5149e6ced 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/EmployeeAddInfoTest/EmployeeAddInfoTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/EmployeeAddInfoTest/EmployeeAddInfoTest.ts @@ -19,7 +19,7 @@ const firstName = faker.person.firstName(); const lastName = faker.person.lastName(); const username = faker.internet.userName(); const password = faker.internet.password(); -const employeeEmail = faker.internet.email(); +const employeeEmail = faker.internet.exampleEmail(); const imgUrl = faker.image.avatar(); const employeeFullName = `${firstName} ${lastName}`; diff --git a/apps/gauzy-e2e/src/support/step_definitions/EmployeeDashboardTest/EmployeeDashboardTest.ts b/apps/gauzy-e2e/src/support/step_definitions/EmployeeDashboardTest/EmployeeDashboardTest.ts index c0d12138cbf..549a7999a1f 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/EmployeeDashboardTest/EmployeeDashboardTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/EmployeeDashboardTest/EmployeeDashboardTest.ts @@ -14,7 +14,7 @@ import * as manageEmployeesPage from '../../Base/pages/ManageEmployees.po'; import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); @@ -26,7 +26,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); let employeeFullName = `${firstName} ${lastName}`; diff --git a/apps/gauzy-e2e/src/support/step_definitions/EstimatesTest/EstimatesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/EstimatesTest/EstimatesTest.ts index 91130c3fe51..e67bcc9af24 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/EstimatesTest/EstimatesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/EstimatesTest/EstimatesTest.ts @@ -18,19 +18,19 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); let street = faker.location.streetAddress(); let website = faker.internet.url(); -let sendEmail = faker.internet.email(); +let sendEmail = faker.internet.exampleEmail(); let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/EventTypesTest/EventTypesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/EventTypesTest/EventTypesTest.ts index 9093de5a81d..d3b7794f90a 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/EventTypesTest/EventTypesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/EventTypesTest/EventTypesTest.ts @@ -16,7 +16,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/ExpensesTest/ExpensesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ExpensesTest/ExpensesTest.ts index 6b82b626a65..2b745be80cc 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ExpensesTest/ExpensesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ExpensesTest/ExpensesTest.ts @@ -21,7 +21,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/GoalsSettingsTest/GoalsGeneralSettingsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/GoalsSettingsTest/GoalsGeneralSettingsTest.ts index 0d84f444f34..7ba65ed24a2 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/GoalsSettingsTest/GoalsGeneralSettingsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/GoalsSettingsTest/GoalsGeneralSettingsTest.ts @@ -18,7 +18,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/GoalsTest/GoalsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/GoalsTest/GoalsTest.ts index 8c5fd3d11d3..931de4a6026 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/GoalsTest/GoalsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/GoalsTest/GoalsTest.ts @@ -18,7 +18,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); @@ -29,7 +29,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/HumanResourcesTest/HumanResourcesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/HumanResourcesTest/HumanResourcesTest.ts index 17ba301c0fa..fc9877f30e5 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/HumanResourcesTest/HumanResourcesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/HumanResourcesTest/HumanResourcesTest.ts @@ -16,7 +16,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/IncomeTest/IncomeTest.ts b/apps/gauzy-e2e/src/support/step_definitions/IncomeTest/IncomeTest.ts index e9cb434a57d..6913c7ba039 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/IncomeTest/IncomeTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/IncomeTest/IncomeTest.ts @@ -17,7 +17,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/InvoicesTest/InvoicesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/InvoicesTest/InvoicesTest.ts index e51f0f08635..0090ee1d2e2 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/InvoicesTest/InvoicesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/InvoicesTest/InvoicesTest.ts @@ -23,16 +23,16 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); let street = faker.location.streetAddress(); let website = faker.internet.url(); -let sendEmail = faker.internet.email(); +let sendEmail = faker.internet.exampleEmail(); // Login with email Given('Login with default credentials', () => { diff --git a/apps/gauzy-e2e/src/support/step_definitions/JobsProposalsTest/JobsProposalsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/JobsProposalsTest/JobsProposalsTest.ts index d8ceac229fa..2a6e3d177bb 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/JobsProposalsTest/JobsProposalsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/JobsProposalsTest/JobsProposalsTest.ts @@ -16,7 +16,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/ManageCandidatesInvitesTest/ManageCandidatesInvitesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ManageCandidatesInvitesTest/ManageCandidatesInvitesTest.ts index 53a50e7133f..3ecf000c347 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ManageCandidatesInvitesTest/ManageCandidatesInvitesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ManageCandidatesInvitesTest/ManageCandidatesInvitesTest.ts @@ -10,7 +10,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); // Login with email Given('Login with default credentials', () => { diff --git a/apps/gauzy-e2e/src/support/step_definitions/ManageEmployeesTest/ManageEmployeesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ManageEmployeesTest/ManageEmployeesTest.ts index 442dfff0ac7..8b5d8937cb6 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ManageEmployeesTest/ManageEmployeesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ManageEmployeesTest/ManageEmployeesTest.ts @@ -16,13 +16,13 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); -let secEmail = faker.internet.email(); +let email = faker.internet.exampleEmail(); +let secEmail = faker.internet.exampleEmail(); let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/ManageInterviewsTest/ManageInterviewsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ManageInterviewsTest/ManageInterviewsTest.ts index b7f37b5c008..51cad5a64a0 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ManageInterviewsTest/ManageInterviewsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ManageInterviewsTest/ManageInterviewsTest.ts @@ -13,7 +13,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); @@ -25,7 +25,7 @@ let empFirstName = faker.person.firstName(); let empLastName = faker.person.lastName(); let empUsername = faker.internet.userName(); let empPassword = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let empImgUrl = faker.image.avatar(); const createRandomInterviewTitleNumber = () => { diff --git a/apps/gauzy-e2e/src/support/step_definitions/ManageUserInvitesTest/ManageUserInvitesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ManageUserInvitesTest/ManageUserInvitesTest.ts index bf225d91064..9db6d2de622 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ManageUserInvitesTest/ManageUserInvitesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ManageUserInvitesTest/ManageUserInvitesTest.ts @@ -9,7 +9,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); // Login with email Given('Login with default credentials and visit Users page', () => { diff --git a/apps/gauzy-e2e/src/support/step_definitions/MyTasksTrackedInTimesheetsTest/MyTasksTrackedInTimesheetsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/MyTasksTrackedInTimesheetsTest/MyTasksTrackedInTimesheetsTest.ts index d8ac48d628b..5382e0c4cd3 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/MyTasksTrackedInTimesheetsTest/MyTasksTrackedInTimesheetsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/MyTasksTrackedInTimesheetsTest/MyTasksTrackedInTimesheetsTest.ts @@ -18,7 +18,7 @@ import { OrganizationTagsPageData } from '../../Base/pagedata/OrganizationTagsPa const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); @@ -31,7 +31,7 @@ let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); let imgUrl = faker.image.avatar(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let employeeFullName = `${firstName} ${lastName}`; diff --git a/apps/gauzy-e2e/src/support/step_definitions/OrganizationDepartmentsTest/OrganizationDepartmentsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/OrganizationDepartmentsTest/OrganizationDepartmentsTest.ts index e406a66de9e..e57ee817451 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/OrganizationDepartmentsTest/OrganizationDepartmentsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/OrganizationDepartmentsTest/OrganizationDepartmentsTest.ts @@ -21,7 +21,7 @@ let empFirstName = faker.person.firstName(); let empLastName = faker.person.lastName(); let empUsername = faker.internet.userName(); let empPassword = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let empImgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/OrganizationEquipmentTest/OrganizationEquipmentTest.ts b/apps/gauzy-e2e/src/support/step_definitions/OrganizationEquipmentTest/OrganizationEquipmentTest.ts index 369bbe581f4..5ece3dc4432 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/OrganizationEquipmentTest/OrganizationEquipmentTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/OrganizationEquipmentTest/OrganizationEquipmentTest.ts @@ -16,7 +16,7 @@ let empFirstName = faker.person.firstName(); let empLastName = faker.person.lastName(); let empUsername = faker.internet.userName(); let empPassword = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let empImgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/OrganizationHelpCenterTest/OrganizationHelpCenterTest.ts b/apps/gauzy-e2e/src/support/step_definitions/OrganizationHelpCenterTest/OrganizationHelpCenterTest.ts index cc345bfadc0..5b2fffb7b49 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/OrganizationHelpCenterTest/OrganizationHelpCenterTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/OrganizationHelpCenterTest/OrganizationHelpCenterTest.ts @@ -19,7 +19,7 @@ let empFirstName = faker.person.firstName(); let empLastName = faker.person.lastName(); let empUsername = faker.internet.userName(); let empPassword = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let empImgUrl = faker.image.avatar(); let desc = faker.lorem.words(); let articleText = faker.lorem.paragraph(); diff --git a/apps/gauzy-e2e/src/support/step_definitions/OrganizationInventoryTest/OrganizationInventoryTest.ts b/apps/gauzy-e2e/src/support/step_definitions/OrganizationInventoryTest/OrganizationInventoryTest.ts index 15718e211c7..979d9ddaec7 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/OrganizationInventoryTest/OrganizationInventoryTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/OrganizationInventoryTest/OrganizationInventoryTest.ts @@ -14,7 +14,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let website = faker.internet.url(); let description = faker.lorem.text(); let city = faker.location.city(); diff --git a/apps/gauzy-e2e/src/support/step_definitions/OrganizationProjectsTest/OrganizationProjectsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/OrganizationProjectsTest/OrganizationProjectsTest.ts index 270c60bc4a8..36ab0c22384 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/OrganizationProjectsTest/OrganizationProjectsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/OrganizationProjectsTest/OrganizationProjectsTest.ts @@ -18,7 +18,7 @@ let empFirstName = faker.person.firstName(); let empLastName = faker.person.lastName(); let empUsername = faker.internet.userName(); let empPassword = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let empImgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/OrganizationPublicPageTest/OrganizationPublicPageTest.ts b/apps/gauzy-e2e/src/support/step_definitions/OrganizationPublicPageTest/OrganizationPublicPageTest.ts index 81464a9feee..b574e7e8371 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/OrganizationPublicPageTest/OrganizationPublicPageTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/OrganizationPublicPageTest/OrganizationPublicPageTest.ts @@ -18,7 +18,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; import { faker } from '@faker-js/faker'; import { OrganizationProjectsPageData } from '../../Base/pagedata/OrganizationProjectsPageData'; -const email = faker.internet.email(); +const email = faker.internet.exampleEmail(); const fullName = faker.person.firstName() + ' ' + faker.person.lastName(); const city = faker.location.city(); const postcode = faker.location.zipCode(); @@ -29,7 +29,7 @@ const firstName = faker.person.firstName(); const lastName = faker.person.lastName(); const username = faker.internet.userName(); const password = faker.internet.password(); -const employeeEmail = faker.internet.email(); +const employeeEmail = faker.internet.exampleEmail(); const imgUrl = faker.image.avatar(); const employeeFullName = `${firstName} ${lastName}`; diff --git a/apps/gauzy-e2e/src/support/step_definitions/OrganizationTeamsTest/OrganizationTeamsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/OrganizationTeamsTest/OrganizationTeamsTest.ts index beb2fb61d68..af4c779751e 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/OrganizationTeamsTest/OrganizationTeamsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/OrganizationTeamsTest/OrganizationTeamsTest.ts @@ -19,7 +19,7 @@ let empFirstName = faker.person.firstName(); let empLastName = faker.person.lastName(); let empUsername = faker.internet.userName(); let empPassword = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let empImgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/PaymentsTest/PaymentsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/PaymentsTest/PaymentsTest.ts index 05716be56e4..7f814fc3462 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/PaymentsTest/PaymentsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/PaymentsTest/PaymentsTest.ts @@ -18,7 +18,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/ProjectTrackedInTimesheetsTest/ProjectTrackedInTimesheetsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ProjectTrackedInTimesheetsTest/ProjectTrackedInTimesheetsTest.ts index ccd6023d144..8c4ce96e938 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ProjectTrackedInTimesheetsTest/ProjectTrackedInTimesheetsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ProjectTrackedInTimesheetsTest/ProjectTrackedInTimesheetsTest.ts @@ -20,7 +20,7 @@ let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); let imgUrl = faker.image.avatar(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let projectName = faker.company.name() diff --git a/apps/gauzy-e2e/src/support/step_definitions/ProposalsTest/ProposalsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ProposalsTest/ProposalsTest.ts index 03ae67cb2c6..abc23f2dea5 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ProposalsTest/ProposalsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ProposalsTest/ProposalsTest.ts @@ -24,7 +24,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/RecurringExpensesTest/RecurringExpensesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/RecurringExpensesTest/RecurringExpensesTest.ts index ea48010e04d..9b75bbdb9ba 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/RecurringExpensesTest/RecurringExpensesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/RecurringExpensesTest/RecurringExpensesTest.ts @@ -16,7 +16,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/RegisterTest/RegisterTest.ts b/apps/gauzy-e2e/src/support/step_definitions/RegisterTest/RegisterTest.ts index daa89acffc3..a2d092df44c 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/RegisterTest/RegisterTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/RegisterTest/RegisterTest.ts @@ -12,7 +12,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); let fullName = faker.person.fullName(); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let pass = faker.internet.password(); let organizationName = faker.company.name(); let taxId = faker.string.alphanumeric(); diff --git a/apps/gauzy-e2e/src/support/step_definitions/ReportsTest/ReportsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/ReportsTest/ReportsTest.ts index 2817bc3b0d9..99c53c2f8b5 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/ReportsTest/ReportsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/ReportsTest/ReportsTest.ts @@ -23,7 +23,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); @@ -34,7 +34,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); let employeeFullName = `${firstName} ${lastName}`; let projectName = faker.company.name() diff --git a/apps/gauzy-e2e/src/support/step_definitions/SalesEstimatesTest/SalesEstimatesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/SalesEstimatesTest/SalesEstimatesTest.ts index 99f808d3fac..0c1146705aa 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/SalesEstimatesTest/SalesEstimatesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/SalesEstimatesTest/SalesEstimatesTest.ts @@ -19,19 +19,19 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); let street = faker.location.streetAddress(); let website = faker.internet.url(); -let sendEmail = faker.internet.email(); +let sendEmail = faker.internet.exampleEmail(); let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/SalesInvoicesTest/SalesInvoicesTest.ts b/apps/gauzy-e2e/src/support/step_definitions/SalesInvoicesTest/SalesInvoicesTest.ts index 7a1dddadd50..025c7dd75f0 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/SalesInvoicesTest/SalesInvoicesTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/SalesInvoicesTest/SalesInvoicesTest.ts @@ -22,16 +22,16 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); let street = faker.location.streetAddress(); let website = faker.internet.url(); -let sendEmail = faker.internet.email(); +let sendEmail = faker.internet.exampleEmail(); // Login with email Given('Login with default credentials', () => { diff --git a/apps/gauzy-e2e/src/support/step_definitions/TimeOffTest/TimeOffTest.ts b/apps/gauzy-e2e/src/support/step_definitions/TimeOffTest/TimeOffTest.ts index d73083fa330..3dc52c48a44 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/TimeOffTest/TimeOffTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/TimeOffTest/TimeOffTest.ts @@ -16,7 +16,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingForClientTest/TimeTrackingForClientTest.ts b/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingForClientTest/TimeTrackingForClientTest.ts index 827a29aa017..f3f80886b6a 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingForClientTest/TimeTrackingForClientTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingForClientTest/TimeTrackingForClientTest.ts @@ -13,7 +13,7 @@ import { waitUntil } from '../../Base/utils/util'; import { TimeTrackingForClientPageData } from '../../Base/pagedata/TimeTrackingForClientPageData'; -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); @@ -26,7 +26,7 @@ let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); let imgUrl = faker.image.avatar(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let employeeFullName = `${firstName} ${lastName}`; diff --git a/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingTest/TimeTrackingTest.ts b/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingTest/TimeTrackingTest.ts index 07194838f38..60a874e6ff9 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingTest/TimeTrackingTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingTest/TimeTrackingTest.ts @@ -18,7 +18,7 @@ import { AddTasksPageData } from '../../Base/pagedata/AddTasksPageData'; import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; import { waitUntil } from '../../Base/utils/util'; -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); @@ -30,7 +30,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); let employeeFullName = `${firstName} ${lastName}`; diff --git a/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingWithPauseTest/TimeTrackingWithPauseTest.ts b/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingWithPauseTest/TimeTrackingWithPauseTest.ts index f06ac73559d..78db6b9c61e 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingWithPauseTest/TimeTrackingWithPauseTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/TimeTrackingWithPauseTest/TimeTrackingWithPauseTest.ts @@ -14,7 +14,7 @@ import { TimeTrackingWithPausePageData } from '../../Base/pagedata/TimeTracingWi const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); @@ -27,7 +27,7 @@ let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); let imgUrl = faker.image.avatar(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let employeeFullName = `${firstName} ${lastName}`; diff --git a/apps/gauzy-e2e/src/support/step_definitions/TimesheetsTest/TimesheetsTest.ts b/apps/gauzy-e2e/src/support/step_definitions/TimesheetsTest/TimesheetsTest.ts index c4459b5ffef..607cd8f8d22 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/TimesheetsTest/TimesheetsTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/TimesheetsTest/TimesheetsTest.ts @@ -21,7 +21,7 @@ import { Given, Then, When, And } from 'cypress-cucumber-preprocessor/steps'; const pageLoadTimeout = Cypress.config('pageLoadTimeout'); -let email = faker.internet.email(); +let email = faker.internet.exampleEmail(); let fullName = faker.person.firstName() + ' ' + faker.person.lastName(); let city = faker.location.city(); let postcode = faker.location.zipCode(); @@ -33,7 +33,7 @@ let firstName = faker.person.firstName(); let lastName = faker.person.lastName(); let username = faker.internet.userName(); let password = faker.internet.password(); -let employeeEmail = faker.internet.email(); +let employeeEmail = faker.internet.exampleEmail(); let imgUrl = faker.image.avatar(); // Login with email diff --git a/apps/gauzy/src/app/@core/components/base-nav-menu/base-nav-menu.component.ts b/apps/gauzy/src/app/@core/components/base-nav-menu/base-nav-menu.component.ts new file mode 100644 index 00000000000..9c04e32448c --- /dev/null +++ b/apps/gauzy/src/app/@core/components/base-nav-menu/base-nav-menu.component.ts @@ -0,0 +1,1048 @@ +import { Directive, OnDestroy, OnInit } from '@angular/core'; +import { merge } from 'rxjs'; +import { filter, tap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { FeatureEnum, IOrganization, PermissionsEnum } from '@gauzy/contracts'; +import { distinctUntilChange } from '@gauzy/common-angular'; +import { NavMenuBuilderService, NavMenuSectionItem } from '../../services/nav-builder'; +import { Store } from '../../services/store.service'; +import { SidebarMenuService } from '../../../@shared/sidebar-menu/sidebar-menu.service'; +import { TranslationBaseComponent } from '../../../@shared/language-base'; + +@UntilDestroy() +@Directive({ + selector: '[gaBaseNavMenu]', +}) +export class BaseNavMenuComponent extends TranslationBaseComponent implements OnInit, OnDestroy { + + constructor( + protected readonly _navMenuBuilderService: NavMenuBuilderService, + protected readonly _store: Store, + protected readonly _sidebarMenuService: SidebarMenuService, + protected readonly _translate: TranslateService, + ) { + super(_translate); + } + + ngOnInit(): void { + this.defineBaseNavMenus(); + } + + ngAfterViewInit() { + merge( + this.translateService.onLangChange.pipe( + tap(() => this.defineBaseNavMenus()), + ), + this._store.selectedOrganization$.pipe( + filter((organization: IOrganization) => !!organization), + distinctUntilChange(), + tap(() => this.defineBaseNavMenus()) + ), + this._store.featureOrganizations$.pipe( + tap(() => this.defineBaseNavMenus()), + ), + this._store.featureTenant$.pipe( + tap(() => this.defineBaseNavMenus()), + ), + this._store.userRolePermissions$.pipe( + tap(() => this.defineBaseNavMenus()), + ) + ).pipe( + untilDestroyed(this) + ).subscribe(); + } + + /** + * Defines the base navigation menus. + */ + private defineBaseNavMenus() { + this._navMenuBuilderService.defineNavMenuSections([ + { + id: 'dashboards', + title: 'Dashboards', + icon: 'fas fa-th', + link: '/pages/dashboard', + pathMatch: 'prefix', + home: true, + data: { + translationKey: 'MENU.DASHBOARDS', + featureKey: FeatureEnum.FEATURE_DASHBOARD + } + }, + { + id: 'focus', + title: 'Focus', + icon: 'fas fa-bullseye', + link: '/pages/dashboard', + pathMatch: 'prefix', + class: 'focus', + hidden: true, + data: { + translationKey: 'MENU.FOCUS', + featureKey: FeatureEnum.FEATURE_DASHBOARD + } + }, + { + id: 'applications', + title: 'Applications', + icon: 'far fa-window-maximize', + link: '/pages/dashboard', + pathMatch: 'prefix', + class: 'application', + hidden: true, + data: { + translationKey: 'MENU.APPLICATIONS', + featureKey: FeatureEnum.FEATURE_DASHBOARD + } + }, + { + id: 'accounting', + title: 'Accounting', + icon: 'far fa-address-card', + data: { + translationKey: 'MENU.ACCOUNTING' + }, + items: [ + { + id: 'accounting-estimates', + title: 'Estimates', + icon: 'far fa-file', + link: '/pages/accounting/invoices/estimates', + data: { + translationKey: 'MENU.ESTIMATES', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.ESTIMATES_VIEW + ], + featureKey: FeatureEnum.FEATURE_ESTIMATE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ESTIMATES_EDIT + ) && { + add: '/pages/accounting/invoices/estimates/add' + }) + } + }, + { + id: 'accounting-estimates-received', + title: 'Estimates Received', + icon: 'fas fa-file-invoice', + link: '/pages/accounting/invoices/received-estimates', + data: { + translationKey: 'MENU.ESTIMATES_RECEIVED', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.ESTIMATES_VIEW + ], + featureKey: FeatureEnum.FEATURE_ESTIMATE_RECEIVED + } + }, + { + id: 'accounting-invoices', + title: 'Invoices', + icon: 'far fa-file-alt', + link: '/pages/accounting/invoices', + pathMatch: 'full', + data: { + translationKey: 'MENU.INVOICES', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.INVOICES_VIEW + ], + featureKey: FeatureEnum.FEATURE_INVOICE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.INVOICES_EDIT + ) && { + add: '/pages/accounting/invoices/add' + }) + } + }, + { + id: 'accounting-invoices-recurring', + title: 'Invoices Recurring', + icon: 'fas fa-exchange-alt fa-rotate-90', + link: '/pages/accounting/invoices/recurring', + pathMatch: 'prefix', + data: { + translationKey: 'MENU.RECURRING_INVOICES', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.INVOICES_VIEW + ], + featureKey: FeatureEnum.FEATURE_INVOICE_RECURRING + } + }, + { + id: 'accounting-invoices-received', + title: 'Invoices Received', + icon: 'fas fa-file-invoice-dollar', + link: '/pages/accounting/invoices/received-invoices', + pathMatch: 'prefix', + data: { + translationKey: 'MENU.INVOICES_RECEIVED', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.INVOICES_VIEW + ], + featureKey: FeatureEnum.FEATURE_INVOICE_RECEIVED + } + }, + { + id: 'accounting-income', + title: 'Income', + icon: 'fas fa-plus-circle', + link: '/pages/accounting/income', + data: { + translationKey: 'MENU.INCOME', + permissionKeys: [ + PermissionsEnum.ORG_INCOMES_VIEW + ], + featureKey: FeatureEnum.FEATURE_INCOME, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_INCOMES_EDIT + ) && { + add: '/pages/accounting/income?openAddDialog=true' + }) + } + }, + { + id: 'accounting-expenses', + title: 'Expenses', + icon: 'fas fa-minus-circle', + link: '/pages/accounting/expenses', + data: { + translationKey: 'MENU.EXPENSES', + permissionKeys: [PermissionsEnum.ORG_EXPENSES_VIEW], + featureKey: FeatureEnum.FEATURE_EXPENSE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_EXPENSES_EDIT + ) && { + add: '/pages/accounting/expenses?openAddDialog=true' + }) + } + }, + { + id: 'accounting-expense-recurring', + title: 'Expense Recurring', + icon: 'fas fa-exchange-alt fa-rotate-90', + link: '/pages/accounting/expense-recurring', + data: { + translationKey: 'ORGANIZATIONS_PAGE.EXPENSE_RECURRING', + permissionKeys: [PermissionsEnum.ORG_EXPENSES_VIEW], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_RECURRING_EXPENSE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_EXPENSES_EDIT + ) && { + add: '/pages/accounting/expense-recurring?openAddDialog=true' + }) + } + }, + { + id: 'accounting-payments', + title: 'Payments', + icon: 'fas fa-cash-register', + link: '/pages/accounting/payments', + data: { + translationKey: 'MENU.PAYMENTS', + permissionKeys: [ + PermissionsEnum.ORG_PAYMENT_VIEW + ], + featureKey: FeatureEnum.FEATURE_PAYMENT, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_PAYMENT_ADD_EDIT + ) && { + add: '/pages/accounting/payments?openAddDialog=true' + }) + } + } + ] + }, + { + id: 'sales', + title: 'Sales', + icon: 'fas fa-chart-line', + link: '/pages/sales', + data: { + translationKey: 'MENU.SALES', + permissionKeys: [ + PermissionsEnum.ORG_PROPOSALS_VIEW + ] + }, + items: [ + { + id: 'sales-proposals', + title: 'Proposals', + icon: 'fas fa-paper-plane', + link: '/pages/sales/proposals', + data: { + translationKey: 'MENU.PROPOSALS', + permissionKeys: [ + PermissionsEnum.ORG_PROPOSALS_VIEW + ], + featureKey: FeatureEnum.FEATURE_PROPOSAL, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_PROPOSALS_EDIT + ) && { + add: '/pages/sales/proposals/register' + }) + } + }, + { + id: 'sales-estimates', + title: 'Estimates', + icon: 'far fa-file', + link: '/pages/sales/invoices/estimates', + data: { + translationKey: 'MENU.ESTIMATES', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.ESTIMATES_VIEW + ], + featureKey: FeatureEnum.FEATURE_PROPOSAL, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ESTIMATES_EDIT + ) && { + add: '/pages/sales/invoices/estimates/add' + }) + } + }, + { + id: 'sales-invoices', + title: 'Invoices', + icon: 'far fa-file-alt', + link: '/pages/sales/invoices', + data: { + translationKey: 'MENU.INVOICES', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.INVOICES_VIEW + ], + featureKey: FeatureEnum.FEATURE_INVOICE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.INVOICES_EDIT + ) && { + add: '/pages/sales/invoices/add' + }) + } + }, + { + id: 'sales-invoices-recurring', + title: 'Invoices Recurring', + icon: 'fas fa-exchange-alt fa-rotate-90', + link: '/pages/sales/invoices/recurring', + data: { + translationKey: 'MENU.RECURRING_INVOICES', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.INVOICES_VIEW + ], + featureKey: FeatureEnum.FEATURE_INVOICE_RECURRING + } + }, + { + id: 'sales-payments', + title: 'Payments', + icon: 'fas fa-cash-register', + link: '/pages/sales/payments', + data: { + translationKey: 'MENU.PAYMENTS', + permissionKeys: [PermissionsEnum.ORG_PAYMENT_VIEW], + featureKey: FeatureEnum.FEATURE_PAYMENT, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_PAYMENT_ADD_EDIT + ) && { + add: '/pages/sales/payments?openAddDialog=true' + }) + } + }, + { + id: 'sales-pipelines', + title: 'Pipelines', + icon: 'fas fa-filter', + link: '/pages/sales/pipelines', + data: { + translationKey: 'MENU.PIPELINES', + permissionKeys: [PermissionsEnum.VIEW_SALES_PIPELINES], + featureKey: FeatureEnum.FEATURE_PIPELINE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.EDIT_SALES_PIPELINES + ) && { + add: '/pages/sales/pipelines?openAddDialog=true' + }) + } + } + ] + }, + { + id: 'tasks', + title: 'Tasks', + icon: 'fas fa-tasks', + link: '/pages/tasks', + data: { + translationKey: 'MENU.TASKS' + }, + items: [ + { + id: 'tasks-dashboard', + title: 'Dashboard', + icon: 'fas fa-th', + link: '/pages/tasks/dashboard', + data: { + translationKey: 'MENU.DASHBOARD', + permissionKeys: [PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_TASK_VIEW], + featureKey: FeatureEnum.FEATURE_DASHBOARD_TASK, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_TASK_ADD + ) && { + add: '/pages/tasks/dashboard?openAddDialog=true' + }) + } + }, + { + id: 'tasks-team', + title: "Team's Tasks", + icon: 'fas fa-user-friends', + link: '/pages/tasks/team', + data: { + translationKey: 'MENU.TEAM_TASKS', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.ORG_TASK_VIEW + ], + featureKey: FeatureEnum.FEATURE_TEAM_TASK, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_TASK_ADD + ) && { + add: '/pages/tasks/team?openAddDialog=true' + }) + } + } + ] + }, + { + id: 'jobs', + title: 'Jobs', + icon: 'fas fa-briefcase', + link: '/pages/jobs', + data: { + translationKey: 'MENU.JOBS', + featureKey: FeatureEnum.FEATURE_JOB + }, + items: [ + { + id: 'jobs-employee', + title: 'Employee', + icon: 'fas fa-user-friends', + link: '/pages/jobs/employee', + data: { + translationKey: 'MENU.EMPLOYEES', + permissionKeys: [ + PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW + ] + } + }, + { + id: 'jobs-proposal-template', + title: 'Proposal Template', + icon: 'far fa-file-alt', + link: '/pages/jobs/proposal-template', + data: { + translationKey: 'MENU.PROPOSAL_TEMPLATE', + permissionKeys: [ + PermissionsEnum.ORG_PROPOSAL_TEMPLATES_VIEW + ], + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_PROPOSAL_TEMPLATES_EDIT + ) && { + add: '/pages/jobs/proposal-template?openAddDialog=true' + }) + } + } + ] + }, + { + id: 'employees', + title: 'Employees', + icon: 'fas fa-user-friends', + data: { + translationKey: 'MENU.EMPLOYEES' + }, + items: [ + { + id: 'employees-manage', + title: 'Manage', + icon: 'fas fa-list', + link: '/pages/employees', + pathMatch: 'full', + data: { + translationKey: 'MENU.MANAGE', + permissionKeys: [ + PermissionsEnum.ORG_EMPLOYEES_VIEW + ], + featureKey: FeatureEnum.FEATURE_EMPLOYEES + } + }, + { + id: 'employees-time-activity', + title: 'Time & Activity', + icon: 'fas fa-chart-line', + link: '/pages/employees/activity', + pathMatch: 'prefix', + data: { + translationKey: 'MENU.TIME_ACTIVITY', + permissionKeys: [ + PermissionsEnum.ADMIN_DASHBOARD_VIEW, + PermissionsEnum.TIME_TRACKER + ], + featureKey: FeatureEnum.FEATURE_EMPLOYEE_TIME_ACTIVITY + } + }, + { + id: 'employees-timesheets', + title: 'Timesheets', + icon: 'far fa-clock', + link: '/pages/employees/timesheets', + pathMatch: 'prefix', + data: { + translationKey: 'MENU.TIMESHEETS', + permissionKeys: [ + PermissionsEnum.ADMIN_DASHBOARD_VIEW, + PermissionsEnum.TIME_TRACKER + ], + featureKey: FeatureEnum.FEATURE_EMPLOYEE_TIMESHEETS + } + }, + { + id: 'employees-appointments', + title: 'Appointments', + icon: 'fas fa-calendar-week', + link: '/pages/employees/appointments', + pathMatch: 'prefix', + data: { + translationKey: 'MENU.APPOINTMENTS', + featureKey: FeatureEnum.FEATURE_EMPLOYEE_APPOINTMENT + } + }, + { + id: 'employees-approvals', + title: 'Approvals', + icon: 'fas fa-repeat', + link: '/pages/employees/approvals', + data: { + translationKey: 'MENU.APPROVALS', + permissionKeys: [ + PermissionsEnum.REQUEST_APPROVAL_VIEW + ], + featureKey: FeatureEnum.FEATURE_EMPLOYEE_APPROVAL, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.REQUEST_APPROVAL_EDIT + ) && { + add: '/pages/employees/approvals?openAddDialog=true' + }) + } + }, + { + id: 'employees-levels', + title: 'Employee Levels', + icon: 'fas fa-chart-bar', + link: `/pages/employees/employee-level`, + data: { + translationKey: 'MENU.EMPLOYEE_LEVEL', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW + ], + featureKey: FeatureEnum.FEATURE_EMPLOYEE_LEVEL + } + }, + { + id: 'employees-positions', + title: 'Positions', + icon: 'fas fa-award', + link: `/pages/employees/positions`, + data: { + translationKey: 'MENU.POSITIONS', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW + ], + featureKey: FeatureEnum.FEATURE_EMPLOYEE_POSITION + } + }, + { + id: 'employees-time-off', + title: 'Time Off', + icon: 'far fa-times-circle', + link: '/pages/employees/time-off', + data: { + translationKey: 'MENU.TIME_OFF', + permissionKeys: [ + PermissionsEnum.ORG_TIME_OFF_VIEW + ], + featureKey: FeatureEnum.FEATURE_EMPLOYEE_TIMEOFF, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_TIME_OFF_VIEW + ) && { + add: '/pages/employees/time-off?openAddDialog=true' + }) + } + }, + { + id: 'employees-recurring-expenses', + title: 'Recurring Expenses', + icon: 'fas fa-exchange-alt fa-rotate-90', + link: '/pages/employees/recurring-expenses', + data: { + translationKey: 'MENU.RECURRING_EXPENSE', + permissionKeys: [ + PermissionsEnum.EMPLOYEE_EXPENSES_VIEW + ], + featureKey: FeatureEnum.FEATURE_EMPLOYEE_RECURRING_EXPENSE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.EMPLOYEE_EXPENSES_EDIT + ) && { + add: '/pages/employees/recurring-expenses?openAddDialog=true' + }) + } + }, + { + id: 'employees-candidates', + title: 'Candidates', + icon: 'fas fa-user-check', + link: '/pages/employees/candidates', + data: { + translationKey: 'MENU.CANDIDATES', + permissionKeys: [ + PermissionsEnum.ORG_CANDIDATES_VIEW + ], + featureKey: FeatureEnum.FEATURE_EMPLOYEE_CANDIDATE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_CANDIDATES_EDIT + ) && { + add: '/pages/employees/candidates?openAddDialog=true' + }) + } + } + ] + }, + { + id: 'organization', + title: 'Organization', + icon: 'fas fa-globe-americas', + data: { + translationKey: 'MENU.ORGANIZATION' + }, + items: [ + { + id: 'organization-equipment', + title: 'Equipment', + icon: 'fas fa-border-all', + link: '/pages/organization/equipment', + data: { + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.ORG_EQUIPMENT_VIEW + ], + translationKey: 'MENU.EQUIPMENT', + featureKey: FeatureEnum.FEATURE_ORGANIZATION_EQUIPMENT, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_EQUIPMENT_EDIT + ) && { + add: '/pages/organization/equipment?openAddDialog=true' + }) + } + }, + { + id: 'organization-inventory', + title: 'Inventory', + icon: 'fas fa-grip-vertical', + link: '/pages/organization/inventory', + pathMatch: 'prefix', + data: { + translationKey: 'MENU.INVENTORY', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW + ], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_INVENTORY, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.INVENTORY_GALLERY_ADD + ) && { + add: '/pages/organization/inventory/create' + }) + } + }, + { + id: 'organization-tags', + title: 'Tags', + icon: 'fas fa-tag', + link: '/pages/organization/tags', + data: { + translationKey: 'MENU.TAGS', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.ORG_TAGS_ADD + ], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_TAG, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_TAGS_ADD + ) && { + add: '/pages/organization/tags?openAddDialog=true' + }) + } + }, + { + id: 'organization-vendors', + title: 'Vendors', + icon: 'fas fa-truck', + link: '/pages/organization/vendors', + data: { + translationKey: 'ORGANIZATIONS_PAGE.VENDORS', + permissionKeys: [ + PermissionsEnum.ALL_ORG_EDIT + ], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_VENDOR, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT + ) && { + add: '/pages/organization/vendors?openAddDialog=true' + }) + } + }, + { + id: 'organization-projects', + title: 'Projects', + icon: 'fas fa-book', + link: `/pages/organization/projects`, + data: { + translationKey: 'ORGANIZATIONS_PAGE.PROJECTS', + permissionKeys: [ + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_PROJECT_VIEW + ], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_PROJECT, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_PROJECT_ADD + ) && { + add: '/pages/organization/projects/create' + }) + } + }, + { + id: 'organization-departments', + title: 'Departments', + icon: ' fas fa-briefcase', + link: `/pages/organization/departments`, + data: { + translationKey: 'ORGANIZATIONS_PAGE.DEPARTMENTS', + permissionKeys: [PermissionsEnum.ALL_ORG_EDIT], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_DEPARTMENT, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT + ) && { + add: '/pages/organization/departments?openAddDialog=true' + }) + } + }, + { + id: 'organization-teams', + title: 'Teams', + icon: 'fas fa-user-friends', + link: `/pages/organization/teams`, + data: { + translationKey: 'ORGANIZATIONS_PAGE.EDIT.TEAMS', + permissionKeys: [ + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_TEAM_VIEW + ], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_TEAM, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_TEAM_EDIT + ) && { + add: '/pages/organization/teams?openAddDialog=true' + }) + } + }, + { + id: 'organization-documents', + title: 'Documents', + icon: 'far fa-file-alt', + link: `/pages/organization/documents`, + data: { + translationKey: 'ORGANIZATIONS_PAGE.DOCUMENTS', + permissionKeys: [ + PermissionsEnum.ALL_ORG_EDIT + ], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_DOCUMENT, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT + ) && { + add: '/pages/organization/documents?openAddDialog=true' + }) + } + }, + { + id: 'organization-employment', + title: 'Employment Types', + icon: 'fas fa-layer-group', + link: `/pages/organization/employment-types`, + data: { + translationKey: 'ORGANIZATIONS_PAGE.EMPLOYMENT_TYPES', + permissionKeys: [ + PermissionsEnum.ALL_ORG_EDIT + ], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_EMPLOYMENT_TYPE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT + ) && { + add: '/pages/organization/employment-types?openAddDialog=true' + }) + } + }, + { + id: 'organization-expense', + title: 'Expense Recurring', + icon: 'fas fa-exchange-alt fa-rotate-90', + link: '/pages/organization/expense-recurring', + data: { + translationKey: 'ORGANIZATIONS_PAGE.EXPENSE_RECURRING', + permissionKeys: [ + PermissionsEnum.ORG_EXPENSES_VIEW + ], + featureKey: FeatureEnum.FEATURE_ORGANIZATION_RECURRING_EXPENSE, + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_EXPENSES_EDIT + ) && { + add: '/pages/organization/expense-recurring?openAddDialog=true' + }) + } + }, + { + id: 'organization-help', + title: 'Help Center', + icon: 'far fa-question-circle', + link: '/pages/organization/help-center', + data: { + translationKey: 'ORGANIZATIONS_PAGE.HELP_CENTER', + featureKey: FeatureEnum.FEATURE_ORGANIZATION_HELP_CENTER + } + } + ] + }, + { + id: 'contacts', + title: 'Contacts', + icon: 'far fa-address-book', + data: { + translationKey: 'MENU.CONTACTS', + permissionKeys: [ + PermissionsEnum.ORG_CONTACT_VIEW, + PermissionsEnum.ALL_ORG_VIEW + ], + featureKey: FeatureEnum.FEATURE_CONTACT + }, + items: [ + { + id: 'contacts-visitors', + title: 'Visitors', + icon: 'fas fa-id-badge', + link: `/pages/contacts/visitors`, + data: { + translationKey: 'CONTACTS_PAGE.VISITORS' + } + }, + { + id: 'contacts-leads', + title: 'Leads', + icon: 'fas fa-id-badge', + link: `/pages/contacts/leads`, + data: { + translationKey: 'CONTACTS_PAGE.LEADS', + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_CONTACT_EDIT + ) && { + add: '/pages/contacts/leads?openAddDialog=true' + }) + } + }, + { + id: 'contacts-customers', + title: 'Customers', + icon: 'fas fa-id-badge', + link: `/pages/contacts/customers`, + data: { + translationKey: 'CONTACTS_PAGE.CUSTOMERS', + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_CONTACT_EDIT + ) && { + add: '/pages/contacts/customers?openAddDialog=true' + }) + } + }, + { + id: 'contacts-clients', + title: 'Clients', + icon: 'fas fa-id-badge', + link: `/pages/contacts/clients`, + data: { + translationKey: 'CONTACTS_PAGE.CLIENTS', + ...(this._store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_CONTACT_EDIT + ) && { + add: '/pages/contacts/clients?openAddDialog=true' + }) + } + } + ] + }, + { + id: 'goals', + title: 'Goals', + icon: 'fab fa-font-awesome-flag', + data: { + translationKey: 'MENU.GOALS' + }, + items: [ + { + id: 'goals-manage', + title: 'Manage', + link: '/pages/goals', + pathMatch: 'full', + icon: 'fas fa-list', + data: { + translationKey: 'MENU.MANAGE', + featureKey: FeatureEnum.FEATURE_GOAL + } + }, + { + id: 'goals-report', + title: 'Report', + link: '/pages/goals/reports', + icon: 'far fa-file-alt', + data: { + translationKey: 'MENU.REPORTS', + featureKey: FeatureEnum.FEATURE_GOAL_REPORT + } + }, + { + id: 'goals-settings', + title: 'Settings', + link: '/pages/goals/settings', + icon: 'fas fa-cog', + data: { + translationKey: 'MENU.SETTINGS', + featureKey: FeatureEnum.FEATURE_GOAL_SETTING + } + } + ] + }, + { + id: 'reports', + title: 'Reports', + icon: 'fas fa-chart-pie', + link: '/pages/reports', + data: { + translationKey: 'MENU.REPORTS', + featureKey: FeatureEnum.FEATURE_REPORT + }, + items: [ + { + id: 'reports-all', + title: 'All Reports', + link: '/pages/reports/all', + icon: 'fas fa-chart-bar', + data: { + translationKey: 'MENU.ALL_REPORTS' + } + }, + ] + } + ]); + } + + /** + * Maps menu sections and their sub-sections recursively. + * + * @param items The menu items to map. + * @returns The mapped menu sections. + */ + public mapMenuSections(items: NavMenuSectionItem[]): NavMenuSectionItem[] { + return items.map((item: NavMenuSectionItem) => this.mapMenuSection(item)); + } + + /** + * Maps a single menu section and its sub-sections recursively. + * + * @param section The menu section to map. + * @returns The mapped menu section. + */ + public mapMenuSection(item: NavMenuSectionItem): NavMenuSectionItem { + const section: NavMenuSectionItem = { + ...item, + title: this.getTranslation(item.data.translationKey), + hidden: item.hidden || this.isSectionHidden(item), + }; + + if (item.items) { + section.children = this.mapMenuSections(item.items); + } + + return section; + } + + /** + * Checks if a menu section should be hidden based on permissions and features. + * + * @param section The menu section to check. + * @returns True if the section should be hidden, false otherwise. + */ + public isSectionHidden(section: NavMenuSectionItem): boolean { + const { data } = section; + let isHidden = false; + + // Check if section should be hidden based on permissions or custom hide function + if (data.permissionKeys || data.hide) { + // If permission keys are provided, check if any permission is granted + const anyPermission = data.permissionKeys ? this._store.hasAnyPermission(...data.permissionKeys) : true; + + // If any permission is not granted or custom hide function returns true, hide the section + if (!anyPermission || (data.hide && data.hide())) { + isHidden = true; + } + } + + // If feature key is provided, check if the feature is enabled + if (data.featureKey && isHidden === false) { + isHidden = !this._store.hasFeatureEnabled(data.featureKey); + } + + // If none of the above conditions are met, the section should not be hidden + return isHidden; + } + + ngOnDestroy(): void { } +} diff --git a/apps/gauzy/src/app/@core/components/common-nav.module.ts b/apps/gauzy/src/app/@core/components/common-nav.module.ts new file mode 100644 index 00000000000..03d45717e2f --- /dev/null +++ b/apps/gauzy/src/app/@core/components/common-nav.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { BaseNavMenuComponent } from './base-nav-menu/base-nav-menu.component'; +import { MainNavMenuComponent } from './main-nav-menu/main-nav-menu.component'; +import { SidebarMenuModule } from '../../@shared/sidebar-menu/sidebar-menu.module'; + +@NgModule({ + imports: [CommonModule, SidebarMenuModule], + declarations: [BaseNavMenuComponent, MainNavMenuComponent], + exports: [BaseNavMenuComponent, MainNavMenuComponent], +}) +export class CommonNavModule { } diff --git a/apps/gauzy/src/app/@core/components/main-nav-menu/main-nav-menu.component.html b/apps/gauzy/src/app/@core/components/main-nav-menu/main-nav-menu.component.html new file mode 100644 index 00000000000..855541efc25 --- /dev/null +++ b/apps/gauzy/src/app/@core/components/main-nav-menu/main-nav-menu.component.html @@ -0,0 +1 @@ + diff --git a/apps/gauzy/src/app/@core/components/main-nav-menu/main-nav-menu.component.scss b/apps/gauzy/src/app/@core/components/main-nav-menu/main-nav-menu.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/gauzy/src/app/@core/components/main-nav-menu/main-nav-menu.component.ts b/apps/gauzy/src/app/@core/components/main-nav-menu/main-nav-menu.component.ts new file mode 100644 index 00000000000..226552d2c0c --- /dev/null +++ b/apps/gauzy/src/app/@core/components/main-nav-menu/main-nav-menu.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { BaseNavMenuComponent } from '../base-nav-menu/base-nav-menu.component'; +import { NavMenuSectionItem } from '../../services/nav-builder'; + +@Component({ + selector: 'ga-main-nav-menu', + templateUrl: './main-nav-menu.component.html', + styleUrls: ['./main-nav-menu.component.scss'], +}) +export class MainNavMenuComponent extends BaseNavMenuComponent implements OnInit { + + public mainMenuConfig$: Observable; + + override ngOnInit(): void { + super.ngOnInit(); // Call the parent class's ngOnInit function + + // Subscribe to the menuConfig$ observable provided by _navMenuBuilderService + this.mainMenuConfig$ = this._navMenuBuilderService.menuConfig$.pipe( + map((sections: NavMenuSectionItem[]) => this.mapMenuSections(sections)) + ); + } +} diff --git a/apps/gauzy/src/app/@core/providers/add-nav-menu-item.ts b/apps/gauzy/src/app/@core/providers/add-nav-menu-item.ts new file mode 100644 index 00000000000..2b03b9c1745 --- /dev/null +++ b/apps/gauzy/src/app/@core/providers/add-nav-menu-item.ts @@ -0,0 +1,78 @@ +import { APP_INITIALIZER, Provider } from "@angular/core"; +import { NavMenuSectionItem } from "../services/nav-builder/nav-builder-types"; +import { NavMenuBuilderService } from "../services/nav-builder"; + +/** +* Factory function to create a provider for adding a navigation menu section (parent menu). +* +* @param config The configuration object representing the new navigation menu section to add. +* @param before (Optional) The identifier of the section before which the new section should be added. +* +* @example +* +* const beforeSectionId = 'dashboard'; +* +* export default [ +* addNavMenuSection({ +* id: 'reports', +* label: 'Reports', +* items: [ +* { +* id: 'report1', +* label: 'Report 1', +* link: '/reports/report1' +* }, +* { +* id: 'report2', +* label: 'Report 2', +* link: '/reports/report2' +* } +* ] +* }, beforeSectionId) +* ]; +* +* @returns The provider configuration. +*/ +export function addNavMenuSection(config: NavMenuSectionItem, before?: string): Provider { + return { + provide: APP_INITIALIZER, + multi: true, + useFactory: (navBuilderService: NavMenuBuilderService) => () => { + navBuilderService.addNavMenuSection(config, before); + }, + deps: [NavMenuBuilderService], + }; +} + + +/** + * Factory function to create a provider for adding a navigation menu item (child menu). + * + * @param config The configuration object representing the new navigation menu item to add. + * @param sectionId The identifier of the section to which the new item should be added. + * @param before (Optional) The identifier of the item before which the new item should be added. + * + * @example + * + * const beforeItemId = 'item1'; + * + * export default [ + * addNavMenuItem({ + * id: 'new-menu-item', + * label: 'New Menu Item', + * link: '/new-menu-item', + * }, 'section1', beforeItemId) + * ]; + * + * @returns The provider configuration. + */ +export function addNavMenuItem(config: NavMenuSectionItem, sectionId: string, before?: string): Provider { + return { + provide: APP_INITIALIZER, + multi: true, + useFactory: (navBuilderService: NavMenuBuilderService) => () => { + navBuilderService.addNavMenuItem(config, sectionId, before); + }, + deps: [NavMenuBuilderService], + }; +} diff --git a/apps/gauzy/src/app/@core/services/nav-builder/index.ts b/apps/gauzy/src/app/@core/services/nav-builder/index.ts new file mode 100644 index 00000000000..386bac0af1c --- /dev/null +++ b/apps/gauzy/src/app/@core/services/nav-builder/index.ts @@ -0,0 +1,2 @@ +export * from './nav-menu-builder.service'; +export * from './nav-builder-types'; diff --git a/apps/gauzy/src/app/@core/services/nav-builder/nav-builder-types.ts b/apps/gauzy/src/app/@core/services/nav-builder/nav-builder-types.ts new file mode 100644 index 00000000000..9efa6067c2d --- /dev/null +++ b/apps/gauzy/src/app/@core/services/nav-builder/nav-builder-types.ts @@ -0,0 +1,54 @@ +import { ActivatedRoute } from "@angular/router"; +import { NbMenuItem } from "@nebular/theme"; +import { FeatureEnum, PermissionsEnum } from "@gauzy/contracts"; + +// Define a type NavMenuBadgeType representing different types of badges. +export type NavMenuBadgeType = 'basic' | 'primary' | 'info' | 'success' | 'warning' | 'danger' | 'control'; + +/** + * A NavMenuSection is a grouping of links in the main (left-hand side) navigation menu bar. + */ +export interface NavMenuSectionItem extends NbMenuItem { + id: string; // Unique identifier for the section + class?: string; // Additional class for styling (optional) + items?: NavMenuSectionItem[]; // Array of NavMenuItem objects representing the links within the section (optional) + onClick?: (event: MouseEvent) => void; // Function to be called when the menu item is clicked (optional) + data: NavMenuItemData; // Data associated with the section +} + +/** + * Data associated with a NavMenuItem or NavMenuSection. + */ +export interface NavMenuItemData { + translationKey: string; // Translation key for the title, mandatory for all items + permissionKeys?: PermissionsEnum[]; // Permissions required to display the item (optional) + featureKey?: FeatureEnum; // Feature key required to display the item (optional) + hide?: () => boolean | boolean; // Function to determine if the item should be hidden (optional) + add?: string; +} + +/** + * Represents the configuration for navigation menu sections. + */ +export interface NavMenuSectionConfig { + config: NavMenuSectionItem; // Configuration for the navigation menu section + before?: string; // (Optional) Identifier of the section before which this section should be inserted +} + +/** + * Represents the configuration for navigation menu items. + */ +export interface NavMenuItemsConfig { + config: NavMenuSectionItem; // Configuration for the navigation menu item + sectionId: string; // Identifier of the section to which this item belongs + before?: string; // (Optional) Identifier of the item before which this item should be inserted +} + +/** + * A function or array that represents a router link definition for a NavMenuItem. + * + * @description + * This type defines a function that takes an ActivatedRoute as input and returns an array representing + * the router link for a NavMenuItem. Alternatively, it can be an array directly representing the router link. + */ +export type RouterLinkDefinition = ((route: ActivatedRoute) => any[]) | any[]; diff --git a/apps/gauzy/src/app/@core/services/nav-builder/nav-menu-builder.service.ts b/apps/gauzy/src/app/@core/services/nav-builder/nav-menu-builder.service.ts new file mode 100644 index 00000000000..fce44168ed4 --- /dev/null +++ b/apps/gauzy/src/app/@core/services/nav-builder/nav-menu-builder.service.ts @@ -0,0 +1,241 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, combineLatest, map, Observable, of, shareReplay } from 'rxjs'; +import { + NavMenuSectionItem, + NavMenuItemsConfig, + NavMenuSectionConfig, +} from './nav-builder-types'; + +@Injectable({ + providedIn: 'root' +}) +export class NavMenuBuilderService { + + // Declare an observable property menuConfig$ of type Observable + public menuConfig$: Observable; + + // Initial configuration of the navigation menu + private initialNavMenuConfig$ = new BehaviorSubject([]); + + // Additional sections that can be added to the navigation menu + private addedNavMenuSections: NavMenuSectionConfig[] = []; + private addedNavMenuSectionsSubject = new BehaviorSubject(this.addedNavMenuSections); + public addedNavMenuSections$: Observable = this.addedNavMenuSectionsSubject.asObservable(); + + // Additional menu items that can be added to the navigation menu + private addedNavMenuItems: NavMenuItemsConfig[] = []; + private removedNavMenuItems: NavMenuItemsConfig[] = []; + private addedNavMenuItemsSubject = new BehaviorSubject(this.addedNavMenuItems); + public addedNavMenuItems$: Observable = this.addedNavMenuItemsSubject.asObservable(); + + constructor() { + this.setupStreams(); + } + + /** + * Defines the navigation menu sections. + * + * @param config An array of NavMenuSection objects representing the navigation menu sections. + */ + defineNavMenuSections(config: NavMenuSectionItem[]) { + this.initialNavMenuConfig$.next(config); + } + + /** + * Adds a new navigation menu section. + * + * @param config The configuration object representing the new navigation menu section to add. + * @param before (Optional) The identifier of the section before which the new section should be added. + */ + addNavMenuSection(config: NavMenuSectionItem, before?: string) { + // Push the new section configuration along with its positioning information into the addedNavMenuSections array + this.addedNavMenuSections.push({ config, before }); + // Emit the updated addedNavMenuSections array to all subscribers + this.addedNavMenuSectionsSubject.next(this.addedNavMenuSections); + } + + /** + * Adds multiple navigation menu sections. + * + * @param configs An array of configuration objects representing the new navigation menu sections to add. + * @param before (Optional) The identifier of the section before which the new section(s) should be added. + */ + addNavMenuSections(configs: NavMenuSectionItem[], before?: string) { + configs.forEach((config: NavMenuSectionItem) => { + // Push the new section configuration along with its positioning information into the addedNavMenuSections array + this.addedNavMenuSections.push({ config, before }); + }); + // Emit the updated addedNavMenuSections array to all subscribers + this.addedNavMenuSectionsSubject.next(this.addedNavMenuSections); + } + + /** + * Adds a new navigation menu item. + * + * @param config The configuration object representing the new navigation menu item to add. + * @param sectionId The identifier of the section to which the new item should be added. + * @param before (Optional) The identifier of the item before which the new item should be added. + */ + addNavMenuItem(config: NavMenuSectionItem, sectionId: string, before?: string) { + // Check if the item already exists + const existingIndex = this.addedNavMenuItems.findIndex(item => + item.config.id === config.id && item.sectionId === sectionId + ); + + if (existingIndex !== -1) { + // Item exists, replace it with the new config + this.addedNavMenuItems[existingIndex] = { config, sectionId, before }; + } else { + // Push each new item configuration along with its positioning information into the addedNavMenuItems array + this.addedNavMenuItems.push({ config, sectionId, before }); + } + // Emit the updated addedNavMenuItems array to all subscribers + this.addedNavMenuItemsSubject.next([...this.addedNavMenuItems]); + } + + /** + * Adds multiple new navigation menu items. + * + * @param configs An array of configuration objects representing the new navigation menu items to add. + * @param sectionId The identifier of the section to which the new items should be added. + * @param before (Optional) The identifier of the item before which the new items should be added. + */ + addNavMenuItems(configs: NavMenuSectionItem[], sectionId: string, before?: string) { + configs.forEach((config: NavMenuSectionItem) => { + // Check if the item already exists + const existingIndex = this.addedNavMenuItems.findIndex((item) => + item.config.id === config.id && item.sectionId === sectionId + ); + + if (existingIndex !== -1) { + // If the item exists, replace it with the new config + this.addedNavMenuItems[existingIndex] = { config, sectionId, before }; + } else { + // Push each new item configuration along with its positioning information into the addedNavMenuItems array + this.addedNavMenuItems.push({ config, sectionId, before }); + } + }); + + // Emit the updated addedNavMenuItems array to all subscribers + this.addedNavMenuItemsSubject.next([...this.addedNavMenuItems]); + } + + /** + * Removes a navigation menu item. + * + * @param itemId The identifier of the item to be removed. + * @param sectionId The identifier of the section from which the item should be removed. + */ + removeNavMenuItem(itemId: string, sectionId: string): void { + const itemIndex = this.addedNavMenuItems.findIndex((item) => item.config.id === itemId && item.sectionId === sectionId); + if (itemIndex !== -1) { + // Check if the item is already present in the removedNavMenuItems array + const existingIndex = this.removedNavMenuItems.findIndex((item) => item.config.id === itemId && item.sectionId === sectionId); + if (existingIndex === -1) { + // Push the removed item into the removedNavMenuItems array + this.removedNavMenuItems.push(this.addedNavMenuItems[itemIndex]); + } + // Remove the item from the addedNavMenuItems array + this.addedNavMenuItems.splice(itemIndex, 1); + // Emit the updated array to subscribers + this.addedNavMenuItemsSubject.next([...this.addedNavMenuItems]); + } + } + + /** + * Removes multiple navigation menu items. + * + * @param itemIds An array of identifiers of the items to be removed. + * @param sectionId The identifier of the section from which the items should be removed. + */ + removeNavMenuItems(itemIds: string[], sectionId: string): void { + itemIds.forEach((itemId: string) => { + this.removeNavMenuItem(itemId, sectionId); + }); + } + + /** + * Sets up streams to dynamically configure the navigation menu based on initial configuration and additions. + */ + private setupStreams(): void { + // Create an observable for section additions + const sectionAdditions$ = this.addedNavMenuSections$; + // Create an observable for item additions + const itemAdditions$ = this.addedNavMenuItems$; + + // Combine the initial configuration and section additions + const combinedConfig$ = combineLatest([this.initialNavMenuConfig$, sectionAdditions$]).pipe( + map(([sections, additions]) => { + const configMap = new Map(); + + // Add initial configurations to the map + sections.forEach((config: NavMenuSectionItem) => configMap.set(config.id, config)); + + // Update or add sections from additions + for (const { config, before } of additions) { + if (configMap.has(config.id)) { + configMap.set(config.id, config); // Update existing config + } else { + const beforeIndex = before ? sections.findIndex((c) => c.id === before) : -1; + if (beforeIndex !== -1) { + sections.splice(beforeIndex, 0, config); // Insert before specified section + } else { + sections.push(config); // Append if before section not found + } + configMap.set(config.id, config); // Add to map + } + } + + return [...configMap.values()]; + }), + shareReplay(1), + ); + + // Combine the combined configuration with item additions to produce the final menu configuration + this.menuConfig$ = combineLatest([combinedConfig$, itemAdditions$, of(this.removedNavMenuItems)]).pipe( + map(([sections, additions, removals]) => { + const sectionMap = new Map(); + + // Populate section map for quick lookup + sections.forEach((section) => sectionMap.set(section.id, section)); + + // Process item deletions + removals.forEach((item: NavMenuItemsConfig) => { + const sectionId = item.sectionId; + const itemIdToRemove = item.config.id; + const section = sectionMap.get(sectionId); + if (section) { + const itemIndex = section.items.findIndex((item) => item.id === itemIdToRemove); + if (itemIndex !== -1) { + section.items.splice(itemIndex, 1); // Remove item from the section + } + } + }); + + // Process item additions + additions.forEach((item: NavMenuItemsConfig) => { + const section = sectionMap.get(item.sectionId); + if (section) { + const { config, before } = item; + const itemIndex = section.items.findIndex((i) => i.id === config.id); + + if (itemIndex !== -1) { + section.items[itemIndex] = config; // Update existing item + } else { + const beforeIndex = before ? section.items.findIndex((i) => i.id === before) : -1; + if (beforeIndex !== -1) { + section.items.splice(beforeIndex, 0, config); // Insert before specified item + } else { + section.items.push(config); // Append if before item not found + } + } + } else { + console.error(`Could not add menu item "${item.config.id}", section "${item.sectionId}" does not exist`); + } + }); + + return sections; + }) + ); + } +} diff --git a/apps/gauzy/src/app/@core/services/users-organizations.service.ts b/apps/gauzy/src/app/@core/services/users-organizations.service.ts index d2f23c5c9ad..08f6cf6c68b 100644 --- a/apps/gauzy/src/app/@core/services/users-organizations.service.ts +++ b/apps/gauzy/src/app/@core/services/users-organizations.service.ts @@ -1,53 +1,66 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { firstValueFrom, Observable } from 'rxjs'; import { + IPagination, IUserOrganization, IUserOrganizationCreateInput, IUserOrganizationFindInput } from '@gauzy/contracts'; -import { firstValueFrom, Observable } from 'rxjs'; +import { toParams } from '@gauzy/common-angular'; import { API_PREFIX } from '../constants/app.constants'; @Injectable() export class UsersOrganizationsService { - constructor(private http: HttpClient) {} + constructor( + private readonly http: HttpClient + ) { } + + /** + * Retrieves user organizations based on specified relations, conditions, and employee inclusion. + * + * @param relations An array of relation names to be eager loaded. + * @param where Optional conditions to filter user organizations. + * @param includeEmployee Specifies whether to include employee information. + * @returns A promise that resolves to a paginated result of user organizations. + */ getAll( - relations?: string[], - findInput?: IUserOrganizationFindInput - ): Promise<{ items: IUserOrganization[]; total: number }> { - const data = JSON.stringify({ relations, findInput }); + relations: string[] = [], + where?: IUserOrganizationFindInput, + includeEmployee: boolean = false + ): Promise> { + // Construct request parameters + const params: any = { relations, where, includeEmployee }; + + // Send HTTP GET request to retrieve user organizations return firstValueFrom( - this.http - .get<{ items: IUserOrganization[]; total: number }>( - `${API_PREFIX}/user-organization`, - { - params: { data } - } - ) + this.http.get>(`${API_PREFIX}/user-organization`, { + params: toParams(params) + }) ); } setUserAsInactive(id: string): Promise { return firstValueFrom( this.http - .put(`${API_PREFIX}/user-organization/${id}`, { - isActive: false - }) + .put(`${API_PREFIX}/user-organization/${id}`, { + isActive: false + }) ); } getUserOrganizationCount(id: string): Promise { return firstValueFrom( this.http - .get(`${API_PREFIX}/user-organization/${id}`) + .get(`${API_PREFIX}/user-organization/${id}`) ); } removeUserFromOrg(id: string): Promise { return firstValueFrom( this.http - .delete(`${API_PREFIX}/user-organization/${id}`) + .delete(`${API_PREFIX}/user-organization/${id}`) ); } diff --git a/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/children-menu-item/children-menu-item.component.html b/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/children-menu-item/children-menu-item.component.html index e69e5dce383..7f94d3c9f55 100644 --- a/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/children-menu-item/children-menu-item.component.html +++ b/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/children-menu-item/children-menu-item.component.html @@ -1,70 +1,53 @@ - + + + -
- - {{ item?.title }} - - -
+ + + + + {{ item?.title }} + + + + + + + +
diff --git a/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/children-menu-item/children-menu-item.component.ts b/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/children-menu-item/children-menu-item.component.ts index c6796a59b05..18748c646fa 100644 --- a/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/children-menu-item/children-menu-item.component.ts +++ b/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/children-menu-item/children-menu-item.component.ts @@ -1,110 +1,171 @@ import { Location } from '@angular/common'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; +import { filter } from 'rxjs/operators'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { IMenuItem } from '../../interface/menu-item.interface'; +@UntilDestroy() @Component({ selector: 'ga-children-menu-item', templateUrl: './children-menu-item.component.html', styleUrls: ['./children-menu-item.component.scss'], }) export class ChildrenMenuItemComponent implements OnInit { + + /** + * Represents a menu item component. + */ private _item: IMenuItem; + get item(): IMenuItem { + return this._item; + } + @Input() set item(value: IMenuItem) { + this._item = value; + } + + /** + * Represents the parent menu item of the current item. + */ private _parent: IMenuItem; - private _selected = false; + get parent(): IMenuItem { + return this._parent; + } + @Input() set parent(value: IMenuItem) { + this._parent = value; + } + + /** + * Indicates whether the menu item is collapsed. + */ private _collapse: boolean; + get collapse() { + return this._collapse; + } + @Input() set collapse(value: boolean) { + this._collapse = value; + } + + /** + * Indicates whether the menu item is selected. + */ + private _selected = false; + get selected(): boolean { + return this._selected; + } + @Input() set selected(value: boolean) { + this._selected = value; + } + + /** + * Indicates whether the mouse is hovering over the menu item. + */ private _mouseHover: boolean; + set mouseHover(value: boolean) { + this._mouseHover = value; + } + get mouseHover() { + return this._mouseHover; + } @Output() public focusItemChange: EventEmitter = new EventEmitter(); constructor( private readonly router: Router, private readonly location: Location - ) {} + ) { } ngOnInit(): void { + // Log and check the current URL this.checkUrl(this.router.url); - this.router.events.subscribe((val) => { - if ((val as NavigationEnd).url) { - this.checkUrl((val as NavigationEnd).url); - } + + // Subscribe to router events and handle NavigationEnd + this.router.events.pipe( + filter((event) => event instanceof NavigationEnd), + untilDestroyed(this) + ).subscribe((event: NavigationEnd) => { + // Log and check the URL when navigation ends + this.checkUrl(event.url); }); } + /** + * Redirects to the specified URL link. + */ public redirectTo(): void { this.router.navigateByUrl(this.item.link); } + /** + * Selects the item and emits an event to focus on it. + * Additionally, redirects to the specified link. + */ public select(): void { + // Emit an event to focus on the item this.focusItemChange.emit({ children: this.item, parent: this.parent, }); + + // Redirect to the specified link this.redirectTo(); } - public checkUrl(url: string) { - if (url === this.item.link) { - this.focusItemChange.emit({ - children: this.item, - parent: this.parent, - }); + /** + * Handles Ctrl + mouse click event to open a link in a new window/tab. + * + * @param event The MouseEvent object representing the mouse click event. + */ + public handleCtrlClick(event: MouseEvent): void { + // Check if Ctrl or Cmd key is pressed + if (event.ctrlKey || event.metaKey) { + // Open the link in a new window/tab + window.open(this.getExternalUrl(this.item.link), '_blank'); + // Prevent default behavior of anchor tag + event.preventDefault(); + } + } + + /** + * Checks if the provided URL matches the link of the current item, + * and emits an event to focus on the item if there is a match. + * @param url The URL to check against the item's link. + */ + public checkUrl(url: string): void { + // Extract only the path part of the URL + const pathOnly = url.split('?')[0]; + + // Check if the path part of the URL matches the item's link + if (pathOnly === this.item.link) { + // Emit an event to focus on the item + this.focusItemChange.emit({ children: this.item, parent: this.parent }); } } + /** + * Prepares the URL for external navigation. + * If the URL is not null or empty, it prepares it for external navigation using Angular's Location service. + * @param url The URL to prepare for external navigation. + * @returns The prepared URL for external navigation. + */ public getExternalUrl(url: string): string { return url ? this.location.prepareExternalUrl(url) : url; } - public add() { - this.focusItemChange.emit({ - children: this.item, - parent: this.parent, - }); + /** + * Emits an event to focus on the current item and navigates to the specified URL for adding. + */ + public add(): void { + this.focusItemChange.emit({ children: this.item, parent: this.parent }); this.router.navigateByUrl(this.item.data.add); } + /** + * Checks if the current item is the last item among its siblings. + * @returns A boolean indicating whether the current item is the last among its siblings. + */ public isLast(): boolean { const last = this.parent.children.slice(-1)[0]; return this.item === last; } - - public get item(): IMenuItem { - return this._item; - } - @Input() - public set item(value: IMenuItem) { - this._item = value; - } - - public get parent(): IMenuItem { - return this._parent; - } - @Input() - public set parent(value: IMenuItem) { - this._parent = value; - } - - public get collapse() { - return this._collapse; - } - @Input() - public set collapse(value: boolean) { - this._collapse = value; - } - - public get selected(): boolean { - return this._selected; - } - @Input() - public set selected(value: boolean) { - this._selected = value; - } - - public set mouseHover(value: boolean) { - this._mouseHover = value; - } - public get mouseHover() { - return this._mouseHover; - } } diff --git a/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.html b/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.html index fd9f0202d2b..313202a0af3 100644 --- a/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.html +++ b/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.html @@ -14,14 +14,15 @@ [ngClass]="onCollapse ? 'collapsed' : ''" (click)="redirectTo()" > - + + {{ item?.title }} + > + {{ item?.title }} + + = new EventEmitter(); @Output() public selectedChange: EventEmitter = new EventEmitter(); constructor( - private readonly router: Router, - private readonly sidebarService: NbSidebarService, - private readonly cdr: ChangeDetectorRef, - private readonly location: Location, - private readonly jitsuService: JitsuService, - private readonly store: Store - ) {} + private readonly _router: Router, + private readonly _sidebarService: NbSidebarService, + private readonly _cdr: ChangeDetectorRef, + private readonly _location: Location, + private readonly _jitsuService: JitsuService, + private readonly _store: Store + ) { } ngOnInit(): void { - this._user = this.store.user; - if (this.item.home) this.selectedChange.emit(this.item); + // Get the user data from the store + this._user = this._store.user; + + // Check if the 'home' property of the 'item' object is truthy + if (this.item.home) { + // If 'home' is truthy, emit an event to notify the parent component + // This emits the 'selectedChange' event with the 'item' as the data + this.selectedChange.emit(this.item); + } } - public onCollapse(event: boolean) { + /** + * Handles the collapse event. + * @param event A boolean indicating whether the item should collapse or not. + */ + public onCollapse(event: boolean): void { + // Update the collapse state based on the event this.collapse = event; } - public focusOn(event: any) { + /** + * Focuses on a specific item. + * @param event The event containing information about the item to focus on. + */ + public focusOn(event: any): void { + // Set the selected children property to the children of the event this.selectedChildren = event.children; - if (this.collapse) this.collapse = !this.collapse; + + // Toggle the collapse state if it's currently collapsed + if (this.collapse) { + this.collapse = !this.collapse; + } + + // Emit the selectedChange event with the parent of the event this.selectedChange.emit(event.parent); - this.cdr.detectChanges(); + + // Manually detect changes using ChangeDetectorRef + this._cdr.detectChanges(); } /** - * Track a click event. - * @param item The item that was clicked. - * @param user The user who clicked the item. + * Track a click event using Jitsu analytics. */ - public async jitsuTrackClick() { + public async jitsuTrackClick(): Promise { + // Prepare the click event data const clickEvent: JitsuAnalyticsEvents = { eventType: JitsuAnalyticsEventsEnum.BUTTON_CLICKED, - url: this.item.url ?? this.item.link, + url: this.item.url ?? this.item.link, // Use either item.url or item.link userId: this._user.id, userEmail: this._user.email, menuItemName: this.item.title, }; - // Identify the user - await this.jitsuService.identify(this._user.id, { + // Identify the user with Jitsu + await this._jitsuService.identify(this._user.id, { email: this._user.email, fullName: this._user.name, timeZone: this._user.timeZone, }); - // Group the user - await this.jitsuService.group(this._user.id, { + // Group the user with Jitsu + await this._jitsuService.group(this._user.id, { email: this._user.email, fullName: this._user.name, timeZone: this._user.timeZone, }); - // Track the click event - await this.jitsuService.trackEvents(clickEvent.eventType, clickEvent); + // Track the click event using Jitsu + await this._jitsuService.trackEvents(clickEvent.eventType, clickEvent); } - public redirectTo() { + /** + * Redirect to a specified URL and track the click event using Jitsu analytics. + */ + public redirectTo(): void { + // Track the click event using Jitsu analytics // We don't await here because we don't want to wait for the analytics to complete before redirecting this.jitsuTrackClick(); - if (!this.item.children) this.router.navigateByUrl(this.item.link); - if (this.item.home) this.router.navigateByUrl(this.item.url); + + // Redirect to the specified URL + if (!this.item.children) { + // If the item doesn't have children, navigate to its link + this._router.navigateByUrl(this.item.link); + } + if (this.item.home) { + // If the item represents the home page, navigate to its URL + this._router.navigateByUrl(this.item.url); + } + + // Emit the selectedChange event to notify parent components this.selectedChange.emit(this.item); - this.cdr.detectChanges(); + + // Manually detect changes using ChangeDetectorRef + this._cdr.detectChanges(); } - public toggleSidebar() { - if (!this.state && !this.item.home) - this.sidebarService.toggle(false, 'menu-sidebar'); + /** + * Toggle the sidebar and perform a redirection if necessary. + */ + public toggleSidebar(): void { + // Check if the sidebar is closed and the current item is not the home page + if (!this.state && !this.item.home) { + // If so, toggle the sidebar to open + this._sidebarService.toggle(false, 'menu-sidebar'); + } + + // Perform redirection this.redirectTo(); } + /** + * Prepare an external URL. + * @param url The URL to prepare. + * @returns The prepared external URL. + */ public getExternalUrl(url: string): string { - return url ? this.location.prepareExternalUrl(url) : url; + return url ? this._location.prepareExternalUrl(url) : url; } + /** + * + */ ngAfterViewChecked(): void { - this.sidebarService - .getSidebarState('menu-sidebar') - .pipe( - tap( - (state) => - (this.state = state === 'expanded' ? true : false) - ) - ) - .subscribe(); - this.cdr.detectChanges(); - } - - @Input() - public set collapse(value) { - this._collapse = value; - } - - @Input() - public set item(value: IMenuItem) { - this._item = value; - } - - @Input() - public set selected(value: boolean) { - this._selected = value; - } - - public set state(value) { - this._state = value; - } - - public set selectedChildren(value: IMenuItem) { - this._selectedChildren = value; - } - - public get collapse() { - return this._collapse; - } - - public get item() { - return this._item; - } - - public get state() { - return this._state; - } - - public get selectedChildren() { - return this._selectedChildren; - } - - public get selected() { - return this._selected; + const state$ = this._sidebarService.getSidebarState('menu-sidebar'); + state$.pipe(tap((state) => (this.state = state === 'expanded' ? true : false))).subscribe(); + this._cdr.detectChanges(); } } diff --git a/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.html b/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.html index 603a7c3c32b..6a47069582a 100644 --- a/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.html +++ b/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.html @@ -1,10 +1,11 @@ diff --git a/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.scss b/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.scss index 097c4ebf1c5..9c8d30a38d7 100644 --- a/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.scss +++ b/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.scss @@ -1,5 +1,5 @@ :host { - div.menu-container { - background-color: transparent; - } + div.menu-container { + background-color: transparent; + } } diff --git a/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.ts b/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.ts index b4230c111b6..cdeec00e609 100644 --- a/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.ts +++ b/apps/gauzy/src/app/@shared/sidebar-menu/sidebar-menu.component.ts @@ -16,25 +16,33 @@ export class SidebarMenuComponent implements AfterContentChecked { @Input() items: IMenuItem[] = []; - constructor( - private readonly cdr: ChangeDetectorRef, - private readonly _sidebarMenuService: SidebarMenuService - ) {} - - ngAfterContentChecked(): void { - this.cdr.detectChanges(); - } - public get selectedItem() { return this._sidebarMenuService.selectedItem; } - public set selectedItem(value: IMenuItem) { this._sidebarMenuService.selectedItem = value; + this._cdr.detectChanges(); } - public focusOn(event: IMenuItem) { + constructor( + private readonly _cdr: ChangeDetectorRef, + private readonly _sidebarMenuService: SidebarMenuService + ) { } + + ngAfterContentChecked(): void { + this._cdr.detectChanges(); + } + + /** + * Sets the selected item in the sidebar menu and triggers change detection. + * + * @param event The menu item to focus on. + */ + public focusOn(event: IMenuItem): void { + // Set the selected item in the sidebar menu this._sidebarMenuService.selectedItem = event; - this.cdr.detectChanges(); + + // Trigger change detection + this._cdr.detectChanges(); } } diff --git a/apps/gauzy/src/app/@shared/table-components/picture-name-tags/picture-name-tags.component.ts b/apps/gauzy/src/app/@shared/table-components/picture-name-tags/picture-name-tags.component.ts index 81131d07f25..bd568baf0b3 100644 --- a/apps/gauzy/src/app/@shared/table-components/picture-name-tags/picture-name-tags.component.ts +++ b/apps/gauzy/src/app/@shared/table-components/picture-name-tags/picture-name-tags.component.ts @@ -28,8 +28,7 @@ import { NotesWithTagsComponent } from '../notes-with-tags/notes-with-tags.compo [style.background]="background(tag?.color)" [style.color]="backgroundContrast(tag?.color)" [text]="tag?.name" - > - + > `, styles: [ @@ -72,20 +71,21 @@ import { NotesWithTagsComponent } from '../notes-with-tags/notes-with-tags.compo styleUrls: ['./picture-name-tags.component.scss'] }) export class PictureNameTagsComponent extends NotesWithTagsComponent { - @Input() - isTags = true; + /** + * Returns the avatar data based on the properties of the current row data. + * + * @returns An object representing the avatar data. + */ + public get avatar(): any { + const { id, employeeId, fullName, name } = this.rowData; + const avatarId = employeeId === id ? id : employeeId; - public get avatar() { - const employeeId = - this.rowData.employeeId === this.rowData.id - ? this.rowData.id - : this.rowData.employeeId; return { ...this.rowData, - id: employeeId ? employeeId : null, - name: this.rowData.fullName - ? this.rowData.fullName - : this.rowData.name, + id: avatarId || null, + name: fullName || name || null }; } + + @Input() isTags = true; } diff --git a/apps/gauzy/src/app/@theme/layouts/one-column/one-column.layout.html b/apps/gauzy/src/app/@theme/layouts/one-column/one-column.layout.html index dbc01c92d34..5908abf1bc9 100644 --- a/apps/gauzy/src/app/@theme/layouts/one-column/one-column.layout.html +++ b/apps/gauzy/src/app/@theme/layouts/one-column/one-column.layout.html @@ -1,4 +1,5 @@ + @@ -7,6 +8,7 @@
+
+ @@ -65,6 +66,7 @@ > +
isConfirmed), - switchMap(() => this._integrationTenantService.delete(integration.id)), - tap(() => this.showDeletionSuccessMessage(integration)), - tap(() => this.subject$.next(true)), - untilDestroyed(this) - ) - .subscribe(); + const dialog$ = this.openConfirmationDialog(); + dialog$.pipe( + filter(isConfirmed => isConfirmed), + switchMap(() => this._integrationTenantService.delete(integration.id)), + tap(() => { + this.showDeletionSuccessMessage(integration); + this.subject$.next(true); + + if (integration.name === IntegrationEnum.GAUZY_AI) { + this.updateAIJobMatchingEntity(); + } + }), + untilDestroyed(this) + ).subscribe(); } /** diff --git a/apps/gauzy/src/app/pages/pages.component.ts b/apps/gauzy/src/app/pages/pages.component.ts index aaf6b2fc15c..3209b2a1dd4 100644 --- a/apps/gauzy/src/app/pages/pages.component.ts +++ b/apps/gauzy/src/app/pages/pages.component.ts @@ -1,5 +1,12 @@ import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute, Data, NavigationEnd, Router } from '@angular/router'; +import { ActivatedRoute, Data, Router } from '@angular/router'; +import { NbMenuItem } from '@nebular/theme'; +import { TranslateService } from '@ngx-translate/core'; +import { merge, pairwise } from 'rxjs'; +import { filter, map, take, tap } from 'rxjs/operators'; +import { NgxPermissionsService } from 'ngx-permissions'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { chain } from 'underscore'; import { FeatureEnum, IOrganization, @@ -8,29 +15,18 @@ import { IntegrationEnum, PermissionsEnum } from '@gauzy/contracts'; -import { NbMenuItem } from '@nebular/theme'; -import { TranslateService } from '@ngx-translate/core'; -import { filter, map, tap } from 'rxjs/operators'; -import { NgxPermissionsService } from 'ngx-permissions'; -import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { chain } from 'underscore'; import { distinctUntilChange, isNotEmpty } from '@gauzy/common-angular'; -import { SelectorService } from '../@core/utils/selector.service'; -import { IJobMatchingEntity, IntegrationEntitySettingServiceStoreService, IntegrationsService, Store, UsersService } from '../@core/services'; +import { + IJobMatchingEntity, + IntegrationEntitySettingServiceStoreService, + IntegrationsService, + Store, + UsersService +} from '../@core/services'; import { ReportService } from './reports/all-report/report.service'; import { AuthStrategy } from '../@core/auth/auth-strategy.service'; import { TranslationBaseComponent } from '../@shared/language-base'; - -interface GaMenuItem extends NbMenuItem { - class?: string; - data: { - translationKey: string; //Translation key for the title, mandatory for all items - permissionKeys?: PermissionsEnum[]; //Check permissions and hide item if any given permission is not present - featureKey?: FeatureEnum; //Check permissions and hide item if any given permission is not present - withOrganizationShortcuts?: boolean; //Declare if the sidebar item has organization level shortcuts - hide?: () => boolean; //Hide the menu item if this returns true - }; -} +import { NavMenuBuilderService, NavMenuSectionItem } from '../@core/services/nav-builder'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -38,7 +34,7 @@ interface GaMenuItem extends NbMenuItem { styleUrls: ['pages.component.scss'], template: ` - + ` @@ -46,12 +42,10 @@ interface GaMenuItem extends NbMenuItem { export class PagesComponent extends TranslationBaseComponent implements AfterViewInit, OnInit, OnDestroy { - public isEmployeeJobMatchingEntity: boolean = false; - public isEmployee: boolean; public organization: IOrganization; public user: IUser; public menu: NbMenuItem[] = []; - public reportMenuItems: NbMenuItem[] = []; + public reportMenuItems: NavMenuSectionItem[] = []; constructor( private readonly router: Router, @@ -59,907 +53,16 @@ export class PagesComponent extends TranslationBaseComponent public readonly translate: TranslateService, private readonly store: Store, private readonly reportService: ReportService, - private readonly selectorService: SelectorService, private readonly ngxPermissionsService: NgxPermissionsService, private readonly usersService: UsersService, private readonly authStrategy: AuthStrategy, private readonly _integrationsService: IntegrationsService, - private readonly _integrationEntitySettingServiceStoreService: IntegrationEntitySettingServiceStoreService + private readonly _integrationEntitySettingServiceStoreService: IntegrationEntitySettingServiceStoreService, + private readonly _navMenuBuilderService: NavMenuBuilderService, ) { super(translate); } - getMenuItems(): GaMenuItem[] { - return [ - { - title: 'Dashboards', - icon: 'fas fa-th', - link: '/pages/dashboard', - pathMatch: 'prefix', - home: true, - data: { - translationKey: 'MENU.DASHBOARDS', - featureKey: FeatureEnum.FEATURE_DASHBOARD - } - }, - { - title: 'Focus', - icon: 'fas fa-bullseye', - link: '/pages/dashboard', - pathMatch: 'prefix', - class: 'focus', - hidden: true, - data: { - translationKey: 'MENU.FOCUS', - featureKey: FeatureEnum.FEATURE_DASHBOARD - } - }, - { - title: 'Applications', - icon: 'far fa-window-maximize', - link: '/pages/dashboard', - pathMatch: 'prefix', - class: 'application', - hidden: true, - data: { - translationKey: 'MENU.APPLICATIONS', - featureKey: FeatureEnum.FEATURE_DASHBOARD - } - }, - { - title: 'Accounting', - icon: 'far fa-address-card', - data: { - translationKey: 'MENU.ACCOUNTING' - }, - children: [ - { - title: 'Estimates', - icon: 'far fa-file', - link: '/pages/accounting/invoices/estimates', - data: { - translationKey: 'MENU.ESTIMATES', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.ESTIMATES_VIEW - ], - featureKey: FeatureEnum.FEATURE_ESTIMATE, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ESTIMATES_EDIT) - ? { add: '/pages/accounting/invoices/estimates/add' } - : {}) - } - }, - { - title: 'Estimates Received', - icon: 'fas fa-file-invoice', - link: '/pages/accounting/invoices/received-estimates', - data: { - translationKey: 'MENU.ESTIMATES_RECEIVED', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.ESTIMATES_VIEW - ], - featureKey: FeatureEnum.FEATURE_ESTIMATE_RECEIVED - } - }, - { - title: 'Invoices', - icon: 'far fa-file-alt', - link: '/pages/accounting/invoices', - pathMatch: 'full', - data: { - translationKey: 'MENU.INVOICES', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.INVOICES_VIEW - ], - featureKey: FeatureEnum.FEATURE_INVOICE, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.INVOICES_EDIT) - ? { add: '/pages/accounting/invoices/add' } - : {}) - } - }, - { - title: 'Invoices Recurring', - icon: 'fas fa-exchange-alt fa-rotate-90', - link: '/pages/accounting/invoices/recurring', - pathMatch: 'prefix', - data: { - translationKey: 'MENU.RECURRING_INVOICES', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.INVOICES_VIEW - ], - featureKey: FeatureEnum.FEATURE_INVOICE_RECURRING - } - }, - { - title: 'Invoices Received', - icon: 'fas fa-file-invoice-dollar', - link: '/pages/accounting/invoices/received-invoices', - pathMatch: 'prefix', - data: { - translationKey: 'MENU.INVOICES_RECEIVED', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.INVOICES_VIEW - ], - featureKey: FeatureEnum.FEATURE_INVOICE_RECEIVED - } - }, - { - title: 'Income', - icon: 'fas fa-plus-circle', - link: '/pages/accounting/income', - data: { - translationKey: 'MENU.INCOME', - permissionKeys: [PermissionsEnum.ORG_INCOMES_VIEW], - featureKey: FeatureEnum.FEATURE_INCOME, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_INCOMES_EDIT - ) - ? { add: '/pages/accounting/income?openAddDialog=true' } - : {}) - } - }, - { - title: 'Expenses', - icon: 'fas fa-minus-circle', - link: '/pages/accounting/expenses', - data: { - translationKey: 'MENU.EXPENSES', - permissionKeys: [PermissionsEnum.ORG_EXPENSES_VIEW], - featureKey: FeatureEnum.FEATURE_EXPENSE, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_EXPENSES_EDIT - ) - ? { add: '/pages/accounting/expenses?openAddDialog=true' } - : {}) - } - }, - { - title: 'Expense Recurring', - icon: 'fas fa-exchange-alt fa-rotate-90', - link: '/pages/accounting/expense-recurring', - data: { - translationKey: - 'ORGANIZATIONS_PAGE.EXPENSE_RECURRING', - permissionKeys: [PermissionsEnum.ORG_EXPENSES_VIEW], - featureKey: - FeatureEnum.FEATURE_ORGANIZATION_RECURRING_EXPENSE, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_EXPENSES_EDIT - ) - ? { add: '/pages/accounting/expense-recurring?openAddDialog=true' } - : {}) - } - }, - { - title: 'Payments', - icon: 'fas fa-cash-register', - link: '/pages/accounting/payments', - data: { - translationKey: 'MENU.PAYMENTS', - permissionKeys: [PermissionsEnum.ORG_PAYMENT_VIEW], - featureKey: FeatureEnum.FEATURE_PAYMENT, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_PAYMENT_ADD_EDIT - ) - ? { add: '/pages/accounting/payments?openAddDialog=true' } - : {}) - } - } - ] - }, - { - title: 'Sales', - icon: 'fas fa-chart-line', - link: '/pages/sales', - data: { - translationKey: 'MENU.SALES', - permissionKeys: [PermissionsEnum.ORG_PROPOSALS_VIEW] - }, - children: [ - { - title: 'Proposals', - icon: 'fas fa-paper-plane', - link: '/pages/sales/proposals', - data: { - translationKey: 'MENU.PROPOSALS', - permissionKeys: [ - PermissionsEnum.ORG_PROPOSALS_VIEW - ], - featureKey: FeatureEnum.FEATURE_PROPOSAL, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_PROPOSALS_EDIT - ) - ? { add: '/pages/sales/proposals/register' } - : {}) - } - }, - { - title: 'Estimates', - icon: 'far fa-file', - link: '/pages/sales/invoices/estimates', - data: { - translationKey: 'MENU.ESTIMATES', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.ESTIMATES_VIEW - ], - featureKey: FeatureEnum.FEATURE_PROPOSAL, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ESTIMATES_EDIT - ) - ? { add: '/pages/sales/invoices/estimates/add' } - : {}) - } - }, - { - title: 'Invoices', - icon: 'far fa-file-alt', - link: '/pages/sales/invoices', - data: { - translationKey: 'MENU.INVOICES', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.INVOICES_VIEW - ], - featureKey: FeatureEnum.FEATURE_INVOICE, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.INVOICES_EDIT) - ? { add: '/pages/sales/invoices/add' } - : {}) - } - }, - { - title: 'Invoices Recurring', - icon: 'fas fa-exchange-alt fa-rotate-90', - link: '/pages/sales/invoices/recurring', - data: { - translationKey: 'MENU.RECURRING_INVOICES', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.INVOICES_VIEW - ], - featureKey: FeatureEnum.FEATURE_INVOICE_RECURRING - } - }, - { - title: 'Payments', - icon: 'fas fa-cash-register', - link: '/pages/sales/payments', - data: { - translationKey: 'MENU.PAYMENTS', - permissionKeys: [PermissionsEnum.ORG_PAYMENT_VIEW], - featureKey: FeatureEnum.FEATURE_PAYMENT, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_PAYMENT_ADD_EDIT - ) - ? { add: '/pages/sales/payments?openAddDialog=true' } - : {}) - } - }, - { - title: 'Pipelines', - icon: 'fas fa-filter', - link: '/pages/sales/pipelines', - data: { - translationKey: 'MENU.PIPELINES', - permissionKeys: [ - PermissionsEnum.VIEW_SALES_PIPELINES - ], - featureKey: FeatureEnum.FEATURE_PIPELINE, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.EDIT_SALES_PIPELINES - ) - ? { add: '/pages/sales/pipelines?openAddDialog=true' } - : {}) - } - } - ] - }, - { - title: 'Tasks', - icon: 'fas fa-tasks', - link: '/pages/tasks', - data: { - translationKey: 'MENU.TASKS' - }, - children: [ - { - title: 'Dashboard', - icon: 'fas fa-th', - link: '/pages/tasks/dashboard', - data: { - translationKey: 'MENU.DASHBOARD', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.ORG_TASK_VIEW - ], - featureKey: FeatureEnum.FEATURE_DASHBOARD_TASK, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_ADD) - ? { add: '/pages/tasks/dashboard?openAddDialog=true' } - : {}) - } - }, - { - title: 'My Tasks', - icon: 'fas fa-user', - link: '/pages/tasks/me', - data: { - translationKey: 'MENU.MY_TASKS', - hide: () => !this.isEmployee, - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.ORG_TASK_VIEW - ], - featureKey: FeatureEnum.FEATURE_MY_TASK, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_ADD) - ? { add: '/pages/tasks/me?openAddDialog=true' } - : {}) - } - }, - { - title: "Team's Tasks", - icon: 'fas fa-user-friends', - link: '/pages/tasks/team', - data: { - translationKey: 'MENU.TEAM_TASKS', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.ORG_TASK_VIEW - ], - featureKey: FeatureEnum.FEATURE_TEAM_TASK, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_ADD) - ? { add: '/pages/tasks/team?openAddDialog=true' } - : {}) - } - } - ] - }, - { - title: 'Jobs', - icon: 'fas fa-briefcase', - link: '/pages/jobs', - data: { - translationKey: 'MENU.JOBS', - featureKey: FeatureEnum.FEATURE_JOB - }, - children: [ - { - title: 'Employee', - icon: 'fas fa-user-friends', - link: '/pages/jobs/employee', - data: { - translationKey: 'MENU.EMPLOYEES', - permissionKeys: [ - PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW - ] - } - }, - /** */ - ...(this.isEmployeeJobMatchingEntity ? [ - { - title: 'Browse', - icon: 'fas fa-list', - link: '/pages/jobs/search', - data: { - translationKey: 'MENU.JOBS_SEARCH', - permissionKeys: [ - PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW, - PermissionsEnum.ORG_JOB_MATCHING_VIEW - ] - } - }, - { - title: 'Matching', - icon: 'fas fa-user', - link: '/pages/jobs/matching', - data: { - translationKey: 'MENU.JOBS_MATCHING', - permissionKeys: [ - PermissionsEnum.ORG_JOB_MATCHING_VIEW - ] - } - }, - ] : []), - { - title: 'Proposal Template', - icon: 'far fa-file-alt', - link: '/pages/jobs/proposal-template', - data: { - translationKey: 'MENU.PROPOSAL_TEMPLATE', - permissionKeys: [ - PermissionsEnum.ORG_PROPOSAL_TEMPLATES_VIEW - ], - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_PROPOSAL_TEMPLATES_EDIT - ) - ? { add: '/pages/jobs/proposal-template?openAddDialog=true' } - : {}) - } - } - ] - }, - { - title: 'Employees', - icon: 'fas fa-user-friends', - data: { - translationKey: 'MENU.EMPLOYEES' - }, - children: [ - { - title: 'Manage', - icon: 'fas fa-list', - link: '/pages/employees', - pathMatch: 'full', - data: { - translationKey: 'MENU.MANAGE', - permissionKeys: [ - PermissionsEnum.ORG_EMPLOYEES_VIEW - ], - featureKey: FeatureEnum.FEATURE_EMPLOYEES - } - }, - { - title: 'Time & Activity', - icon: 'fas fa-chart-line', - link: '/pages/employees/activity', - pathMatch: 'prefix', - data: { - translationKey: 'MENU.TIME_ACTIVITY', - permissionKeys: [ - PermissionsEnum.ADMIN_DASHBOARD_VIEW, - PermissionsEnum.TIME_TRACKER - ], - featureKey: FeatureEnum.FEATURE_EMPLOYEE_TIME_ACTIVITY - } - }, - { - title: 'Timesheets', - icon: 'far fa-clock', - link: '/pages/employees/timesheets', - pathMatch: 'prefix', - data: { - translationKey: 'MENU.TIMESHEETS', - permissionKeys: [ - PermissionsEnum.ADMIN_DASHBOARD_VIEW, - PermissionsEnum.TIME_TRACKER - ], - featureKey: FeatureEnum.FEATURE_EMPLOYEE_TIMESHEETS - } - }, - { - title: 'Appointments', - icon: 'fas fa-calendar-week', - link: '/pages/employees/appointments', - pathMatch: 'prefix', - data: { - translationKey: 'MENU.APPOINTMENTS', - featureKey: FeatureEnum.FEATURE_EMPLOYEE_APPOINTMENT - } - }, - { - title: 'Approvals', - icon: 'fas fa-repeat', - link: '/pages/employees/approvals', - data: { - translationKey: 'MENU.APPROVALS', - permissionKeys: [ - PermissionsEnum.REQUEST_APPROVAL_VIEW - ], - featureKey: FeatureEnum.FEATURE_EMPLOYEE_APPROVAL, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.REQUEST_APPROVAL_EDIT - ) - ? { add: '/pages/employees/approvals?openAddDialog=true' } - : {}) - } - }, - { - title: 'Employee Levels', - icon: 'fas fa-chart-bar', - link: `/pages/employees/employee-level`, - data: { - translationKey: 'MENU.EMPLOYEE_LEVEL', - permissionKeys: [PermissionsEnum.ALL_ORG_VIEW], - featureKey: FeatureEnum.FEATURE_EMPLOYEE_LEVEL - } - }, - { - title: 'Positions', - icon: 'fas fa-award', - link: `/pages/employees/positions`, - data: { - translationKey: 'MENU.POSITIONS', - permissionKeys: [PermissionsEnum.ALL_ORG_VIEW], - featureKey: FeatureEnum.FEATURE_EMPLOYEE_POSITION - } - }, - { - title: 'Time Off', - icon: 'far fa-times-circle', - link: '/pages/employees/time-off', - data: { - translationKey: 'MENU.TIME_OFF', - permissionKeys: [PermissionsEnum.ORG_TIME_OFF_VIEW], - featureKey: FeatureEnum.FEATURE_EMPLOYEE_TIMEOFF, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_TIME_OFF_VIEW - ) - ? { add: '/pages/employees/time-off?openAddDialog=true' } - : {}) - } - }, - { - title: 'Recurring Expenses', - icon: 'fas fa-exchange-alt fa-rotate-90', - link: '/pages/employees/recurring-expenses', - data: { - translationKey: 'MENU.RECURRING_EXPENSE', - permissionKeys: [ - PermissionsEnum.EMPLOYEE_EXPENSES_VIEW - ], - featureKey: FeatureEnum.FEATURE_EMPLOYEE_RECURRING_EXPENSE, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.EMPLOYEE_EXPENSES_EDIT - ) - ? { add: '/pages/employees/recurring-expenses?openAddDialog=true' } - : {}) - } - }, - { - title: 'Candidates', - icon: 'fas fa-user-check', - link: '/pages/employees/candidates', - data: { - translationKey: 'MENU.CANDIDATES', - permissionKeys: [ - PermissionsEnum.ORG_CANDIDATES_VIEW - ], - featureKey: FeatureEnum.FEATURE_EMPLOYEE_CANDIDATE, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_CANDIDATES_EDIT - ) - ? { add: '/pages/employees/candidates?openAddDialog=true' } - : {}) - } - } - ] - }, - { - title: 'Organization', - icon: 'fas fa-globe-americas', - data: { - translationKey: 'MENU.ORGANIZATION', - withOrganizationShortcuts: true - }, - children: [ - { - title: 'Manage', - icon: 'fas fa-globe-americas', - pathMatch: 'prefix', - data: { - organizationShortcut: true, - permissionKeys: [PermissionsEnum.ALL_ORG_EDIT], - urlPrefix: `/pages/organizations/edit/`, - urlPostfix: '', - translationKey: 'MENU.MANAGE', - featureKey: FeatureEnum.FEATURE_ORGANIZATION - } - }, - { - title: 'Equipment', - icon: 'fas fa-border-all', - link: '/pages/organization/equipment', - data: { - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.ORG_EQUIPMENT_VIEW - ], - translationKey: 'MENU.EQUIPMENT', - featureKey: FeatureEnum.FEATURE_ORGANIZATION_EQUIPMENT, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_EQUIPMENT_EDIT - ) - ? { add: '/pages/organization/equipment?openAddDialog=true' } - : {}) - } - }, - { - title: 'Inventory', - icon: 'fas fa-grip-vertical', - link: '/pages/organization/inventory', - pathMatch: 'prefix', - data: { - translationKey: 'MENU.INVENTORY', - permissionKeys: [PermissionsEnum.ALL_ORG_VIEW], - featureKey: FeatureEnum.FEATURE_ORGANIZATION_INVENTORY, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.INVENTORY_GALLERY_ADD - ) - ? { add: '/pages/organization/inventory/create' } - : {}) - } - }, - { - title: 'Tags', - icon: 'fas fa-tag', - link: '/pages/organization/tags', - data: { - translationKey: 'MENU.TAGS', - permissionKeys: [ - PermissionsEnum.ALL_ORG_VIEW, - PermissionsEnum.ORG_TAGS_ADD - ], - featureKey: FeatureEnum.FEATURE_ORGANIZATION_TAG, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TAGS_ADD) - ? { add: '/pages/organization/tags?openAddDialog=true' } - : {}) - } - }, - { - title: 'Vendors', - icon: 'fas fa-truck', - link: '/pages/organization/vendors', - data: { - translationKey: 'ORGANIZATIONS_PAGE.VENDORS', - permissionKeys: [PermissionsEnum.ALL_ORG_EDIT], - featureKey: FeatureEnum.FEATURE_ORGANIZATION_VENDOR, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT) - ? { add: '/pages/organization/vendors?openAddDialog=true' } - : {}) - } - }, - { - title: 'Projects', - icon: 'fas fa-book', - link: `/pages/organization/projects`, - data: { - translationKey: 'ORGANIZATIONS_PAGE.PROJECTS', - permissionKeys: [ - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_PROJECT_VIEW - ], - featureKey: FeatureEnum.FEATURE_ORGANIZATION_PROJECT, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_PROJECT_ADD - ) - ? { add: '/pages/organization/projects/create' } - : {}) - } - }, - { - title: 'Departments', - icon: ' fas fa-briefcase', - link: `/pages/organization/departments`, - data: { - translationKey: 'ORGANIZATIONS_PAGE.DEPARTMENTS', - permissionKeys: [PermissionsEnum.ALL_ORG_EDIT], - featureKey: FeatureEnum.FEATURE_ORGANIZATION_DEPARTMENT, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT) - ? { add: '/pages/organization/departments?openAddDialog=true' } - : {}) - } - }, - { - title: 'Teams', - icon: 'fas fa-user-friends', - link: `/pages/organization/teams`, - data: { - translationKey: 'ORGANIZATIONS_PAGE.EDIT.TEAMS', - permissionKeys: [ - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_TEAM_VIEW - ], - featureKey: FeatureEnum.FEATURE_ORGANIZATION_TEAM, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TEAM_EDIT) - ? { add: '/pages/organization/teams?openAddDialog=true' } - : {}) - } - }, - { - title: 'Documents', - icon: 'far fa-file-alt', - link: `/pages/organization/documents`, - data: { - translationKey: 'ORGANIZATIONS_PAGE.DOCUMENTS', - permissionKeys: [PermissionsEnum.ALL_ORG_EDIT], - featureKey: FeatureEnum.FEATURE_ORGANIZATION_DOCUMENT, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT) - ? { add: '/pages/organization/documents?openAddDialog=true' } - : {}) - } - }, - { - title: 'Employment Types', - icon: 'fas fa-layer-group', - link: `/pages/organization/employment-types`, - data: { - translationKey: - 'ORGANIZATIONS_PAGE.EMPLOYMENT_TYPES', - permissionKeys: [PermissionsEnum.ALL_ORG_EDIT], - featureKey: FeatureEnum.FEATURE_ORGANIZATION_EMPLOYMENT_TYPE, - ...(this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT) - ? { add: '/pages/organization/employment-types?openAddDialog=true' } - : {}) - } - }, - { - title: 'Expense Recurring', - icon: 'fas fa-exchange-alt fa-rotate-90', - link: '/pages/organization/expense-recurring', - data: { - translationKey: - 'ORGANIZATIONS_PAGE.EXPENSE_RECURRING', - permissionKeys: [PermissionsEnum.ORG_EXPENSES_VIEW], - featureKey: FeatureEnum.FEATURE_ORGANIZATION_RECURRING_EXPENSE, - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_EXPENSES_EDIT - ) - ? { add: '/pages/organization/expense-recurring?openAddDialog=true' } - : {}) - } - }, - { - title: 'Help Center', - icon: 'far fa-question-circle', - link: '/pages/organization/help-center', - data: { - translationKey: 'ORGANIZATIONS_PAGE.HELP_CENTER', - featureKey: - FeatureEnum.FEATURE_ORGANIZATION_HELP_CENTER - } - } - ] - }, - { - title: 'Contacts', - icon: 'far fa-address-book', - data: { - translationKey: 'MENU.CONTACTS', - permissionKeys: [ - PermissionsEnum.ORG_CONTACT_VIEW, - PermissionsEnum.ALL_ORG_VIEW - ], - featureKey: FeatureEnum.FEATURE_CONTACT - }, - children: [ - { - title: 'Visitors', - icon: 'fas fa-id-badge', - link: `/pages/contacts/visitors`, - data: { - translationKey: 'CONTACTS_PAGE.VISITORS' - } - }, - { - title: 'Leads', - icon: 'fas fa-id-badge', - link: `/pages/contacts/leads`, - data: { - translationKey: 'CONTACTS_PAGE.LEADS', - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_CONTACT_EDIT - ) - ? { - add: '/pages/contacts/leads?openAddDialog=true' - } - : {}) - } - }, - { - title: 'Customers', - icon: 'fas fa-id-badge', - link: `/pages/contacts/customers`, - data: { - translationKey: 'CONTACTS_PAGE.CUSTOMERS', - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_CONTACT_EDIT - ) - ? { - add: '/pages/contacts/customers?openAddDialog=true' - } - : {}) - } - }, - { - title: 'Clients', - icon: 'fas fa-id-badge', - link: `/pages/contacts/clients`, - data: { - translationKey: 'CONTACTS_PAGE.CLIENTS', - ...(this.store.hasAnyPermission( - PermissionsEnum.ALL_ORG_EDIT, - PermissionsEnum.ORG_CONTACT_EDIT - ) - ? { - add: '/pages/contacts/clients?openAddDialog=true' - } - : {}) - } - } - ] - }, - { - title: 'Goals', - icon: 'fab fa-font-awesome-flag', - data: { - translationKey: 'MENU.GOALS' - }, - children: [ - { - title: 'Manage', - link: '/pages/goals', - pathMatch: 'full', - icon: 'fas fa-list', - data: { - translationKey: 'MENU.MANAGE', - featureKey: FeatureEnum.FEATURE_GOAL - } - }, - { - title: 'Report', - link: '/pages/goals/reports', - icon: 'far fa-file-alt', - data: { - translationKey: 'MENU.REPORTS', - featureKey: FeatureEnum.FEATURE_GOAL_REPORT - } - }, - { - title: 'Settings', - link: '/pages/goals/settings', - icon: 'fas fa-cog', - data: { - translationKey: 'MENU.SETTINGS', - featureKey: FeatureEnum.FEATURE_GOAL_SETTING - } - } - ] - }, - { - title: 'Reports', - icon: 'fas fa-chart-pie', - link: '/pages/reports', - data: { - translationKey: 'MENU.REPORTS', - featureKey: FeatureEnum.FEATURE_REPORT - }, - children: [ - { - title: 'All Reports', - link: '/pages/reports/all', - icon: 'fas fa-chart-bar', - data: { - translationKey: 'MENU.ALL_REPORTS' - } - }, - ...this.reportMenuItems - ] - } - ]; - } - async ngOnInit() { this.route.data .pipe( @@ -977,21 +80,26 @@ export class PagesComponent extends TranslationBaseComponent ) .subscribe(); await this._createEntryPoint(); - this._applyTranslationOnSmartTable(); - this.store.user$ - .pipe( - filter((user: IUser) => !!user), - tap((user: IUser) => this.isEmployee = !!user.employee), - untilDestroyed(this) - ) - .subscribe(); + this.store.selectedOrganization$.pipe( + filter((organization: IOrganization) => !!organization), + distinctUntilChange(), + pairwise(), // Pair each emitted value with the previous one + tap(([organization]: [IOrganization, IOrganization]) => { + // Remove the specified menu items for previous selected organization + this._navMenuBuilderService.removeNavMenuItems( + // Define the base item IDs + this.getReportMenuBaseItemIds().map((itemId) => `${itemId}-${organization.id}`), 'reports' + ); + }), + untilDestroyed(this) + ).subscribe(); this.store.selectedOrganization$ .pipe( filter((organization: IOrganization) => !!organization), distinctUntilChange(), - tap((organization: IOrganization) => (this.organization = organization)), + tap((organization: IOrganization) => this.organization = organization), tap(() => this.getReportsMenus()), tap(() => this.getIntegrationEntitySettings()), untilDestroyed(this) @@ -1000,89 +108,223 @@ export class PagesComponent extends TranslationBaseComponent this.store.userRolePermissions$ .pipe( - filter((permissions: IRolePermission[]) => - isNotEmpty(permissions) - ), - map((permissions) => - permissions.map(({ permission }) => permission) - ), - tap((permissions) => - this.ngxPermissionsService.loadPermissions(permissions) - ), + filter((permissions: IRolePermission[]) => isNotEmpty(permissions)), + map((permissions) => permissions.map(({ permission }) => permission)), + tap((permissions) => this.ngxPermissionsService.loadPermissions(permissions)), untilDestroyed(this) ) - .subscribe(() => { - this.loadItems( - this.selectorService.showSelectors(this.router.url) - .showOrganizationShortcuts - ); - }); - this.router.events - .pipe(filter((event) => event instanceof NavigationEnd)) - .pipe(untilDestroyed(this)) - .subscribe((e) => { - this.loadItems( - this.selectorService.showSelectors(e['url']) - .showOrganizationShortcuts - ); - }); - this.reportService.menuItems$ - .pipe(distinctUntilChange(), untilDestroyed(this)) - .subscribe((menuItems) => { - if (menuItems) { - this.reportMenuItems = chain(menuItems) - .values() - .map((item) => { - return { - title: item.name, - link: `/pages/reports/${item.slug}`, - icon: item.iconClass, - data: { - translationKey: `${item.name}` - } - }; - }) - .value(); - } else { - this.reportMenuItems = []; - } + .subscribe(); - this.menu = this.getMenuItems(); - this.loadItems( - this.selectorService.showSelectors(this.router.url) - .showOrganizationShortcuts - ); - }); - this.store.featureOrganizations$ - .pipe(untilDestroyed(this)) - .subscribe(() => { - this.loadItems( - this.selectorService.showSelectors(this.router.url) - .showOrganizationShortcuts - ); - }); - this.store.featureTenant$.pipe(untilDestroyed(this)).subscribe(() => { - this.loadItems( - this.selectorService.showSelectors(this.router.url) - .showOrganizationShortcuts - ); + this.reportService.menuItems$.pipe( + distinctUntilChange(), + untilDestroyed(this) + ).subscribe((menuItems) => { + if (menuItems) { + this.reportMenuItems = chain(menuItems) + .values() + .map((item) => { + return { + id: item.slug + `-${this.organization?.id}`, + title: item.name, + link: `/pages/reports/${item.slug}`, + icon: item.iconClass, + data: { + translationKey: `${item.name}` + } + }; + }) + .value(); + } else { + this.reportMenuItems = []; + } + this.addOrganizationReportsMenuItems(); }); - this.menu = this.getMenuItems(); } + /** + * Executes after the view initialization. + */ ngAfterViewInit(): void { - this._integrationEntitySettingServiceStoreService.jobMatchingEntity$ - .pipe( + merge( + this._integrationEntitySettingServiceStoreService.jobMatchingEntity$.pipe( distinctUntilChange(), // Ensure that only distinct changes are considered filter(({ currentValue }: IJobMatchingEntity) => !!currentValue), // Filter out falsy values tap(({ currentValue }: IJobMatchingEntity) => { // Update component properties based on the current job matching entity settings - this.isEmployeeJobMatchingEntity = !!currentValue.sync && !!currentValue.isActive; - this.menu = this.getMenuItems(); // Recreate menu items based on the updated settings - }), - // Handling the component lifecycle to avoid memory leaks - untilDestroyed(this) - ).subscribe(); + const isEmployeeJobMatchingEntity = !!currentValue.sync && !!currentValue.isActive; + + if (isEmployeeJobMatchingEntity) { + this.addJobsNavigationMenuItems(); + } else { + this.removeJobsNavigationMenuItems(); + } + }) + ), + this.store.user$.pipe( + take(1), + distinctUntilChange(), + filter((user: IUser) => !!user && !!user.employee?.id), + tap(() => this.addTasksNavigationMenuItems()) + ), + this.store.selectedOrganization$.pipe( + distinctUntilChange(), + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => this.addOrganizationManageMenuItem(organization)) + ) + ).pipe( + untilDestroyed(this) + ).subscribe(); + } + + /** + * Adds report menu items to the organization's navigation menu. + */ + private addOrganizationReportsMenuItems() { + if (!this.organization) { + // Handle the case where this.organization is not defined + console.warn('Organization not defined. Unable to add/remove menu items.'); + return; + } + const { id: organizationId } = this.organization; + + // Remove the specified menu items for current selected organization + // Note: We need to remove old menus before constructing new menus for the organization. + this._navMenuBuilderService.removeNavMenuItems( + // Define the base item IDs + this.getReportMenuBaseItemIds().map((itemId) => `${itemId}-${organizationId}`), 'reports' + ); + + // Validate if reportMenuItems is an array and has elements + if (!Array.isArray(this.reportMenuItems) || this.reportMenuItems.length === 0) { + return; + } + + // Iterate over each report and add it to the navigation menu + try { + this.reportMenuItems.forEach((report: NavMenuSectionItem) => { + // Validate the structure of each report item + if (report && report.id && report.title) { + this._navMenuBuilderService.addNavMenuItem({ + id: report.id, // Unique identifier for the menu item + title: report.title, // The title of the menu item + icon: report.icon, // The icon class for the menu item, using FontAwesome in this case + link: report.link, // The link where the menu item directs + data: report.data, + }, 'reports'); // The id of the section where this item should be added + } + }); + } catch (error) { + console.error('Error adding report menu items', error); + } + } + + /** + * Retrieves the base item IDs for the report menu. + * These IDs represent the default menu items that are available in the report menu. + * @returns An array containing the base item IDs. + */ + public getReportMenuBaseItemIds() { + // Define the base item IDs + return [ + 'amounts-owed', // Outstanding amounts + 'apps-urls', // Applications and URLs + 'client-budgets', // Budgets per client + 'daily-limits', // Daily spending limits + 'expense', // Expense reports + 'manual-time-edits', // Edits in time logs + 'payments', // Payment transactions + 'project-budgets', // Budgets per project + 'time-activity', // Time-based activities + 'weekly', // Weekly summaries + 'weekly-limits' // Weekly spending limits + ]; + } + + /** + * Adds navigation menu item for managing organization. + */ + private addOrganizationManageMenuItem(organization: IOrganization): void { + this._navMenuBuilderService.addNavMenuItem({ + id: 'organization-manage', // Unique identifier for the menu item + title: 'Manage', // The title of the menu item + icon: 'fas fa-globe-americas', // The icon class for the menu item, using FontAwesome in this case + link: `/pages/organizations/edit/${organization?.id}`, // The link where the menu item directs + pathMatch: 'prefix', + data: { + translationKey: 'MENU.MANAGE', + permissionKeys: [ + PermissionsEnum.ALL_ORG_EDIT + ], // Key for translation (i18n) + featureKey: FeatureEnum.FEATURE_ORGANIZATION // + } + }, 'organization', 'organization-equipment'); // The id of the section where this item should be added + } + + /** + * Adds navigation menu items for tasks. + */ + private addTasksNavigationMenuItems(): void { + this._navMenuBuilderService.addNavMenuItem({ + id: 'tasks-my-tasks', // Unique identifier for the menu item + title: 'My Tasks', // The title of the menu item + icon: 'fas fa-user', // The icon class for the menu item, using FontAwesome in this case + link: '/pages/tasks/me', // The link where the menu item directs + data: { + translationKey: 'MENU.MY_TASKS', // Key for translation (i18n) + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.ORG_TASK_VIEW + ], // Array of permission keys required for this item + featureKey: FeatureEnum.FEATURE_MY_TASK, // + ...(this.store.hasAnyPermission( + PermissionsEnum.ALL_ORG_EDIT, + PermissionsEnum.ORG_TASK_ADD + ) && { + add: '/pages/tasks/me?openAddDialog=true' // + }) + } + }, 'tasks', 'tasks-team'); // The id of the section where this item should be added + } + + /** + * Add navigation menu items for jobs browse and matching. + */ + private addJobsNavigationMenuItems(): void { + this._navMenuBuilderService.addNavMenuItems([ + { + id: 'jobs-browse', // Unique identifier for the menu item + title: 'Browse', // The title of the menu item + icon: 'fas fa-list', // The icon class for the menu item, using FontAwesome in this case + link: '/pages/jobs/search', // The link where the menu item directs + data: { + translationKey: 'MENU.JOBS_SEARCH', // Key for translation (i18n) + permissionKeys: [ + PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW, + PermissionsEnum.ORG_JOB_MATCHING_VIEW + ] // Array of permission keys required for this item + } + }, + { + id: 'jobs-matching', // Unique identifier for the menu item + title: 'Matching', // The title of the menu item + icon: 'fas fa-user', // The icon class for the menu item, using FontAwesome in this case + link: '/pages/jobs/matching', // The link where the menu item directs + data: { + translationKey: 'MENU.JOBS_MATCHING', // Key for translation (i18n) + permissionKeys: [ + PermissionsEnum.ORG_JOB_MATCHING_VIEW + ] // Array of permission keys required for this item + } + } + ], 'jobs', 'jobs-proposal-template'); // The id of the section where this item should be added + } + + /** + * Removes the navigation menu items related to jobs. + */ + private removeJobsNavigationMenuItems(): void { + // Remove the specified menu items related to jobs from the 'jobs' section + this._navMenuBuilderService.removeNavMenuItems(['jobs-browse', 'jobs-matching'], 'jobs'); } /** @@ -1111,6 +353,10 @@ export class PagesComponent extends TranslationBaseComponent this._integrationEntitySettingServiceStoreService.updateAIJobMatchingEntity(integration$).subscribe(); } + /** + * + * @returns + */ async getReportsMenus() { if (!this.organization) { return; @@ -1123,10 +369,6 @@ export class PagesComponent extends TranslationBaseComponent tenantId, organizationId }); - this.loadItems( - this.selectorService.showSelectors(this.router.url) - .showOrganizationShortcuts - ); } /* @@ -1169,57 +411,5 @@ export class PagesComponent extends TranslationBaseComponent ); } - loadItems(withOrganizationShortcuts: boolean) { - this.menu.forEach((item) => { - this.refreshMenuItem(item, withOrganizationShortcuts); - }); - } - - refreshMenuItem(item, withOrganizationShortcuts) { - item.title = this.getTranslation(item.data.translationKey); - if (item.data.permissionKeys || item.data.hide) { - const anyPermission = item.data.permissionKeys - ? item.data.permissionKeys.reduce((permission, key) => { - return this.store.hasPermission(key) || permission; - }, false) - : true; - - item.hidden = - !anyPermission || (item.data.hide && item.data.hide()); - - if (anyPermission && item.data.organizationShortcut) { - item.hidden = !withOrganizationShortcuts || !this.organization; - if (!item.hidden) { - item.link = - item.data.urlPrefix + - this.organization.id + - item.data.urlPostfix; - } - } - } - - // enabled/disabled features from here - if (item.data.hasOwnProperty('featureKey') && item.hidden !== true) { - const { featureKey } = item.data; - const enabled = !this.store.hasFeatureEnabled(featureKey); - item.hidden = enabled || (item.data.hide && item.data.hide()); - } - - if (item.children) { - item.children.forEach((childItem) => { - this.refreshMenuItem(childItem, withOrganizationShortcuts); - }); - } - } - - private _applyTranslationOnSmartTable() { - this.translate.onLangChange.pipe(untilDestroyed(this)).subscribe(() => { - this.loadItems( - this.selectorService.showSelectors(this.router.url) - .showOrganizationShortcuts - ); - }); - } - ngOnDestroy() { } } diff --git a/apps/gauzy/src/app/pages/pages.module.ts b/apps/gauzy/src/app/pages/pages.module.ts index bbc46dea721..95520f4c46c 100644 --- a/apps/gauzy/src/app/pages/pages.module.ts +++ b/apps/gauzy/src/app/pages/pages.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { NbMenuModule, NbToastrModule, NbSpinnerModule, NbIconModule } from '@nebular/theme'; import { FeatureToggleModule as NgxFeatureToggleModule } from 'ngx-feature-toggle'; - import { ThemeModule } from '../@theme/theme.module'; import { PagesComponent } from './pages.component'; import { PagesRoutingModule } from './pages-routing.module'; @@ -9,7 +8,7 @@ import { MiscellaneousModule } from './miscellaneous/miscellaneous.module'; import { AuthService } from '../@core/services/auth.service'; import { RoleGuard } from '../@core/guards'; import { TranslateModule } from '../@shared/translate/translate.module'; -import { SidebarMenuModule } from '../@shared/sidebar-menu/sidebar-menu.module'; +import { CommonNavModule } from '../@core/components/common-nav.module'; @NgModule({ imports: [ @@ -22,7 +21,7 @@ import { SidebarMenuModule } from '../@shared/sidebar-menu/sidebar-menu.module'; ThemeModule, MiscellaneousModule, TranslateModule, - SidebarMenuModule + CommonNavModule ], declarations: [PagesComponent], providers: [AuthService, RoleGuard] diff --git a/apps/gauzy/src/app/pages/pipelines/pipeline-form/pipeline-form.component.ts b/apps/gauzy/src/app/pages/pipelines/pipeline-form/pipeline-form.component.ts index 8345bbdf74b..253a5b68336 100644 --- a/apps/gauzy/src/app/pages/pipelines/pipeline-form/pipeline-form.component.ts +++ b/apps/gauzy/src/app/pages/pipelines/pipeline-form/pipeline-form.component.ts @@ -1,14 +1,8 @@ import { Component, Input, OnInit } from '@angular/core'; -import { - IOrganization, - IPipelineCreateInput, - IUserOrganization -} from '@gauzy/contracts'; -import { UsersOrganizationsService } from '../../../@core/services/users-organizations.service'; -import { Store } from '../../../@core/services/store.service'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { PipelinesService } from '../../../@core/services/pipelines.service'; import { NbDialogRef } from '@nebular/theme'; +import { IOrganization, IPipeline, IPipelineCreateInput } from '@gauzy/contracts'; +import { PipelinesService } from '../../../@core/services/pipelines.service'; @Component({ templateUrl: './pipeline-form.component.html', @@ -16,37 +10,23 @@ import { NbDialogRef } from '@nebular/theme'; selector: 'ga-pipeline-form' }) export class PipelineFormComponent implements OnInit { - @Input() - pipeline: IPipelineCreateInput & { id?: string }; + @Input() pipeline: IPipelineCreateInput & { id?: string }; - userOrganizations: IUserOrganization[]; form: UntypedFormGroup; icon: string; isActive: boolean; organization: IOrganization; constructor( - public dialogRef: NbDialogRef, - private usersOrganizationsService: UsersOrganizationsService, - private pipelinesService: PipelinesService, - private fb: UntypedFormBuilder, - private store: Store + public readonly dialogRef: NbDialogRef, + private readonly pipelinesService: PipelinesService, + private readonly fb: UntypedFormBuilder ) { } ngOnInit(): void { const { id, isActive } = this.pipeline; - const { userId } = this.store; + isActive === undefined ? (this.isActive = true) : (this.isActive = isActive); - isActive === undefined - ? (this.isActive = true) - : (this.isActive = isActive); - - this.usersOrganizationsService - .getAll(['organization'], { - userId, - tenantId: this.pipeline.tenantId - }) - .then(({ items }) => (this.userOrganizations = items)); this.form = this.fb.group({ organizationId: [ this.pipeline.organizationId || '', @@ -61,20 +41,32 @@ export class PipelineFormComponent implements OnInit { }); } + /** + * + */ setIsActive() { this.isActive = !this.isActive; } - persist(): void { - const { - value, - value: { id } - } = this.form; + /** + * + */ + async persist(): Promise { + try { + const { value, value: { id } } = this.form; + let entity: IPipeline; + + // Determine whether to create or update based on the presence of an ID + if (id) { + entity = await this.pipelinesService.update(id, value); + } else { + entity = await this.pipelinesService.create(value); + } - Promise.race([ - id - ? this.pipelinesService.update(id, value) - : this.pipelinesService.create(value) - ]).then((entity) => this.dialogRef.close(entity)); + // Close the dialog with the returned entity + this.dialogRef.close(entity); + } catch (error) { + console.error(`Error occurred while persisting data: ${error.message}`); + } } } diff --git a/apps/gauzy/src/app/pages/reports/all-report/all-report/all-report.component.html b/apps/gauzy/src/app/pages/reports/all-report/all-report/all-report.component.html index 417ab8cccca..784e4e9de99 100644 --- a/apps/gauzy/src/app/pages/reports/all-report/all-report/all-report.component.html +++ b/apps/gauzy/src/app/pages/reports/all-report/all-report/all-report.component.html @@ -8,17 +8,14 @@

-
- - - - {{ reportCategory.name }} -
+
+ + + + {{ reportCategory.name }} +
-
+
@@ -27,24 +24,24 @@
{{ report?.name }}
{{ report?.description}}
-
+
- +
diff --git a/apps/gauzy/src/app/pages/reports/all-report/all-report/all-report.component.ts b/apps/gauzy/src/app/pages/reports/all-report/all-report/all-report.component.ts index 8fb804e2374..0d7b2aecac1 100644 --- a/apps/gauzy/src/app/pages/reports/all-report/all-report/all-report.component.ts +++ b/apps/gauzy/src/app/pages/reports/all-report/all-report/all-report.component.ts @@ -41,28 +41,36 @@ export class AllReportComponent implements OnInit { .subscribe(); } - updateShowInMenu(isEnabled: boolean, report): void { - const { tenantId } = this.store.user; - const { id: organizationId } = this.organization; - this.reportService - .updateReport({ + /** + * Updates the 'show in menu' status of a report. + * + * @param isEnabled Indicates whether the report should be shown in the menu. + * @param report The report to update. + */ + async updateShowInMenu(isEnabled: boolean, report): Promise { + try { + const { id: organizationId, tenantId } = this.organization; + + await this.reportService.updateReport({ reportId: report.id, organizationId, tenantId, isEnabled - }) - .then(() => { - this.reportService.getReportMenuItems({ - organizationId, - tenantId - }); }); + + await this.reportService.getReportMenuItems({ + organizationId, + tenantId + }); + } catch (error) { + console.error(`Error occurred while updating 'show in menu' status: ${error.message}`); + } } /** - * Organization all reports + * Retrieves all reports for the current organization. * - * @returns + * @returns A promise that resolves when reports are successfully retrieved. */ async getReports() { if (!this.organization) { @@ -81,12 +89,10 @@ export class AllReportComponent implements OnInit { const { items = [] } = await this.reportService.getReports(request); const categories = chain(items).groupBy('categoryId'); - this.reportCategories = categories - .map((reports: IReport[]) => ({ - ...reports[0].category, - reports - })) - .value(); + this.reportCategories = categories.map((reports: IReport[]) => ({ + ...reports[0].category, + reports + })).value(); } catch (error) { console.log('Error while retrieving report with category', error); } finally { diff --git a/apps/gauzy/src/app/pages/tags/tags.component.ts b/apps/gauzy/src/app/pages/tags/tags.component.ts index e1501bc0c6b..00cb9e95f64 100644 --- a/apps/gauzy/src/app/pages/tags/tags.component.ts +++ b/apps/gauzy/src/app/pages/tags/tags.component.ts @@ -273,9 +273,9 @@ export class TagsComponent extends PaginationFilterBaseComponent type: 'string', width: '25%', filter: false, - componentInitFunction: (instance: TagsColorComponent, cell: Cell) => { + valuePrepareFunction: (_: any, cell: Cell) => { const item = cell.getRow().getData(); - instance.value = this.getCounter(item); + return this.getCounter(item); } } } diff --git a/apps/gauzy/src/app/pages/users/users.component.html b/apps/gauzy/src/app/pages/users/users.component.html index 0332aec0d77..1b630796874 100644 --- a/apps/gauzy/src/app/pages/users/users.component.html +++ b/apps/gauzy/src/app/pages/users/users.component.html @@ -62,9 +62,9 @@

>

- +
diff --git a/apps/gauzy/src/app/pages/users/users.component.ts b/apps/gauzy/src/app/pages/users/users.component.ts index c90c233f0b7..206b6a0f4e3 100644 --- a/apps/gauzy/src/app/pages/users/users.component.ts +++ b/apps/gauzy/src/app/pages/users/users.component.ts @@ -18,14 +18,7 @@ import { NbDialogService } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; import { Cell, LocalDataSource } from 'angular2-smart-table'; import { filter, tap } from 'rxjs/operators'; -import { - debounceTime, - firstValueFrom, - Subject, - of as observableOf, - map, - finalize -} from 'rxjs'; +import { debounceTime, firstValueFrom, Subject } from 'rxjs'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { distinctUntilChange } from '@gauzy/common-angular'; import { @@ -46,9 +39,9 @@ import { PaginationFilterBaseComponent } from '../../@shared/pagination/pagination-filter-base.component'; import { TagsColorFilterComponent } from '../../@shared/table-filters'; +import { EmailComponent, RoleComponent } from '../../@shared/table-components'; import { monthNames } from '../../@core/utils/date'; import { EmployeeWorkStatusComponent } from '../employees/table-components'; -import { EmailComponent, RoleComponent } from '../../@shared/table-components'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -181,9 +174,7 @@ export class UsersComponent extends PaginationFilterBaseComponent } private get _isGridLayout(): boolean { - return ( - this.componentLayoutStyleEnum.CARDS_GRID === this.dataLayoutStyle - ); + return (this.componentLayoutStyleEnum.CARDS_GRID === this.dataLayoutStyle); } selectUser({ isSelected, data }) { @@ -197,9 +188,7 @@ export class UsersComponent extends PaginationFilterBaseComponent if (data && data.role === RolesEnum.SUPER_ADMIN) { this.disableButton = !this.hasSuperAdminPermission; - this.selectedUser = this.hasSuperAdminPermission - ? this.selectedUser - : null; + this.selectedUser = this.hasSuperAdminPermission ? this.selectedUser : null; } } @@ -211,13 +200,10 @@ export class UsersComponent extends PaginationFilterBaseComponent if (data.user.firstName || data.user.lastName) { this.userName = data.user.firstName + ' ' + data.user.lastName; } - this.toastrService.success( - 'NOTES.ORGANIZATIONS.ADD_NEW_USER_TO_ORGANIZATION', - { - username: this.userName.trim(), - orgname: this.store.selectedOrganization.name - } - ); + this.toastrService.success('NOTES.ORGANIZATIONS.ADD_NEW_USER_TO_ORGANIZATION', { + username: this.userName.trim(), + orgname: this.store.selectedOrganization.name + }); this._refresh$.next(true); this.subject$.next(true); } @@ -314,14 +300,8 @@ export class UsersComponent extends PaginationFilterBaseComponent * User belongs multiple organizations -> remove user from Organization * */ - const count = - await this.userOrganizationsService.getUserOrganizationCount( - userOrganizationId - ); - const confirmationMessage = - count === 1 - ? 'FORM.DELETE_CONFIRMATION.DELETE_USER' - : 'FORM.DELETE_CONFIRMATION.REMOVE_USER'; + const count = await this.userOrganizationsService.getUserOrganizationCount(userOrganizationId); + const confirmationMessage = count === 1 ? 'FORM.DELETE_CONFIRMATION.DELETE_USER' : 'FORM.DELETE_CONFIRMATION.REMOVE_USER'; this.dialogService .open(DeleteConfirmationComponent, { @@ -350,76 +330,115 @@ export class UsersComponent extends PaginationFilterBaseComponent }); } - private async getUsers() { - const { tenantId } = this.store.user; - const { id: organizationId } = this.organization; - const { activePage, itemsPerPage } = this.getPagination(); - - const organizations: IUserOrganization[] = []; + /** + * Fetches users from user organizations, maps them to the required format, and loads them into the smart table. + */ + private async getUsers(): Promise { this.loading = true; - observableOf( - ( - await this.userOrganizationsService.getAll( - ['user', 'user.role', 'user.tags', 'user.employee'], - { organizationId, tenantId } - ) - ).items - ) - .pipe( - map((organizations: IUserOrganization[]) => - organizations - .filter( - (organization: IUserOrganization) => - organization.isActive - ) - .filter( - (organization: IUserOrganization) => - organization.user.role - ) - ), - tap((users: IUserOrganization[]) => - organizations.push(...users) - ), - untilDestroyed(this), - finalize(() => (this.loading = false)) - ) - .subscribe(); - const users = []; - for (const { - id: userOrganizationId, - user, - isActive - } of organizations) { - users.push({ + try { + const organizations = await this._fetchUserOrganizations(); + + // Mapping fetched organizations to required user format + const users = organizations.map(({ id: userOrganizationId, user, isActive }) => ({ id: user.id, fullName: user.name, email: user.email, tags: user.tags, imageUrl: user.imageUrl, role: user.role, - isActive: isActive, - userOrganizationId: userOrganizationId, + isActive, + userOrganizationId, ...this.employeeMapper(user.employee) - }); + })); + + // Initialize Smart Table and load users + this.loadUsersToSmartTable(users); + } catch (error) { + console.error('Error while getting organization users', error?.message); + this.toastrService.danger(error); + } finally { + this.loading = false; } + } + + /** + * Loads users into the smart table with pagination and updates pagination. + * + * @param users The array of users to load into the smart table. + * @param activePage The active page for pagination. + * @param itemsPerPage The number of items per page for pagination. + */ + private loadUsersToSmartTable(users: any[]): void { + const { activePage, itemsPerPage } = this.getPagination(); + this.sourceSmartTable.setPaging(activePage, itemsPerPage, false); this.sourceSmartTable.load(users); + + // Load Grid Data this._loadDataGridLayout(); + + // Updates pagination + this.updatePagination(); + } + + /** + * Updates pagination information based on the current state of the smart table. + */ + private updatePagination(): void { + // Update pagination with total count of items in the smart table this.setPagination({ ...this.getPagination(), totalItems: this.sourceSmartTable.count() }); } - private async _loadDataGridLayout() { - if (this._isGridLayout) { - this.users.push(...(await this.sourceSmartTable.getElements())); - this.users = this.users.filter( - (user, index, self) => - index === self.findIndex(({ id }) => user.id === id) - ); + /** + * Fetches user organizations with necessary relations. + * + * @returns A promise that resolves to an array of IUserOrganization. + */ + private async _fetchUserOrganizations(): Promise { + // If organization is not available, return undefined + if (!this.organization) { + return; + } + + // Destructure organization properties for readability + const { id: organizationId, tenantId } = this.organization; + + // Fetch user organizations with required relations + const userOrganizations = await this.userOrganizationsService.getAll( + ['user', 'user.role', 'user.tags'], + { organizationId, tenantId }, + true + ); + + // Filter user organizations based on isActive and user role + return userOrganizations.items.filter( + (organization) => organization.isActive && organization.user.role + ); + } + + /** + * Loads unique user data into the users array if the grid layout is enabled. + */ + private async _loadDataGridLayout(): Promise { + // Check if grid layout is enabled + if (!this._isGridLayout) { + return; // Exit early if grid layout is disabled } + + // Retrieve elements from the source smart table + const elements = await this.sourceSmartTable.getElements(); + + // Filter unique users based on their IDs + const uniqueUsers = elements.filter((user, index, self) => + index === self.findIndex(({ id }) => user.id === id) + ); + + // Add unique users to the users array + this.users.push(...uniqueUsers); } private _loadSmartTableSettings() { @@ -495,7 +514,10 @@ export class UsersComponent extends PaginationFilterBaseComponent }; } - private _applyTranslationOnSmartTable() { + /** + * Subscribes to language change events and reloads smart table settings accordingly. + */ + private _applyTranslationOnSmartTable(): void { this.translateService.onLangChange .pipe( tap(() => this._loadSmartTableSettings()), @@ -514,26 +536,39 @@ export class UsersComponent extends PaginationFilterBaseComponent }); } - - private employeeMapper(employee: IEmployee) { - if (employee) { - const { endWork, startedWorkOn, isTrackingEnabled, id } = employee; - return { - employeeId: id, - endWork: endWork ? new Date(endWork) : '', - workStatus: endWork - ? new Date(endWork).getDate() + - ' ' + - monthNames[new Date(endWork).getMonth()] + - ' ' + - new Date(endWork).getFullYear() - : '', - startedWorkOn, - isTrackingEnabled - }; - } else { + /** + * Maps an employee object to a simplified format. + * + * @param employee The employee object to be mapped. + * @returns An object containing mapped employee properties. + */ + private employeeMapper(employee: IEmployee): any { + if (!employee) { return {}; } + + const { endWork, startedWorkOn, isTrackingEnabled, id } = employee; + + return { + employeeId: id, + endWork: endWork ? new Date(endWork) : null, + workStatus: endWork ? this.formatDate(new Date(endWork)) : '', + startedWorkOn, + isTrackingEnabled + }; + } + + /** + * Formats a date in the format "DD Month YYYY". + * + * @param date The date object to be formatted. + * @returns A string representing the formatted date. + */ + private formatDate(date: Date): string { + const day = date.getDate(); + const month = monthNames[date.getMonth()]; + const year = date.getFullYear(); + return `${day} ${month} ${year}`; } ngOnDestroy() { } diff --git a/apps/gauzy/src/assets/i18n/bg.json b/apps/gauzy/src/assets/i18n/bg.json index e9139b93fef..04a81f1245e 100644 --- a/apps/gauzy/src/assets/i18n/bg.json +++ b/apps/gauzy/src/assets/i18n/bg.json @@ -800,37 +800,37 @@ "REGISTER_AS_EMPLOYEE_TOOLTIP": "You must be employee in order to be able to track time, create split expenses and use other employees related features." }, "MENU": { - "PIPELINES": "Pipelines", + "PIPELINES": "Потоци", "DASHBOARD": "Табло", - "DASHBOARDS": "Dashboards", - "APPOINTMENTS": "Appointments", - "ACCOUNTING": "Accounting", + "DASHBOARDS": "Табла за управление", + "APPOINTMENTS": "Срещи", + "ACCOUNTING": "Счетоводство", "INCOME": "Доход", "EXPENSES": "Разходи", - "RECURRING_EXPENSE": "Recurring Expenses", + "RECURRING_EXPENSE": "Повтарящи се Разходи", "POSITIONS": "Позиции", - "INTEGRATIONS": "Apps & Integrations", - "UPWORK": "Upwork", + "INTEGRATIONS": "Интеграции", + "UPWORK": "Апуърк", "PROPOSALS": "Предложения", - "TIME_OFF": "Почивка", - "APPROVALS": "Approvals", + "TIME_OFF": "Отпуска", + "APPROVALS": "Одобрения", "HELP": "Помощ", "ABOUT": "За нас", - "CONTACTS": "Contacts", + "CONTACTS": "Контакти", "ADMIN": "Администратор", - "EMPLOYEE_LEVEL": "Employee Level", + "EMPLOYEE_LEVEL": "Ниво на Служителя", "EMPLOYEES": "Служители", "MANAGE": "Управление", - "CANDIDATES": "Candidates", + "CANDIDATES": "Кандидати", "ORGANIZATIONS": "Организации", "SETTINGS": "Настройки", "GENERAL": "Основни", - "EMAIL_HISTORY": "Email History", + "EMAIL_HISTORY": "История на Имейлите", "USERS": "Потребители", "ROLES": "Роли и Позволения", "DANGER_ZONE": "Опасна Зона", - "FILE_STORAGE": "File Storage", - "INVITE_PEOPLE": "Invite People", + "FILE_STORAGE": "Файлово Хранилище", + "INVITE_PEOPLE": "Покани Хора", "IMPORT_EXPORT": { "IMPORT_EXPORT": "Import/Export", "IMPORT_EXPORT_DATA": "Import / Export Data", @@ -1034,48 +1034,48 @@ "ALL_ENTITIES": "ALL Entities" }, "TAGS": "Тагове", - "LANGUAGE": "Език (Language)", - "LANGUAGES": "Languages", - "EQUIPMENT": "Equipment", - "EQUIPMENT_SHARING": "Equipment Sharing", - "TASKS": "Tasks", - "TASKS_SETTINGS": "Настройки", - "INVOICES": "Invoices", - "ORGANIZATION": "Organization", - "TENANT": "Tenant", - "RECURRING_INVOICES": "Invoices Recurring", - "INVOICES_RECEIVED": "Invoices Received", - "ESTIMATES_RECEIVED": "Estimates Received", - "ESTIMATES": "Estimates", - "MY_TASKS": "My Tasks", - "JOBS": "Jobs", - "PROPOSAL_TEMPLATE": "Proposal Template", - "JOBS_SEARCH": "Browse", - "JOBS_MATCHING": "Matching", - "TEAM_TASKS": "Team's Tasks", - "TIME_ACTIVITY": "Time & Activity", - "TIMESHEETS": "Timesheets", - "SCHEDULES": "Schedules", - "EMAIL_TEMPLATES": "Email Templates", - "REPORTS": "Reports", - "GOALS": "Goals", - "ALL_REPORTS": "All Reports", - "TIME_REPORTS": "Time Report", - "WEEKLY_TIME_REPORTS": "Weekly Report", - "ACCOUNTING_REPORTS": "Accounting Reports", - "PAYMENT_GATEWAYS": "Payment Gateways", - "SMS_GATEWAYS": "SMS Gateways", - "CUSTOM_SMTP": "Custom SMTP", - "INVENTORY": "Inventory", - "SALES": "Sales", - "PAYMENTS": "Payments", - "FEATURES": "Features", - "ACCOUNTING_TEMPLATES": "Accounting Templates", - "FOCUS": "Focus", - "APPLICATIONS": "Applications", + "LANGUAGE": "Език", + "LANGUAGES": "Езици", + "EQUIPMENT": "Оборудване", + "EQUIPMENT_SHARING": "Споделяне на Оборудване", + "TASKS": "Задачи", + "TASKS_SETTINGS": "Настройки за Задачи", + "INVOICES": "Фактури", + "ORGANIZATION": "Организация", + "TENANT": "Наемател", + "RECURRING_INVOICES": "Повтарящи се Фактури", + "INVOICES_RECEIVED": "Получени Фактури", + "ESTIMATES_RECEIVED": "Получени Оценки", + "ESTIMATES": "Оценки", + "MY_TASKS": "Моите Задачи", + "JOBS": "Работни Места", + "PROPOSAL_TEMPLATE": "Шаблон за Предложение", + "JOBS_SEARCH": "Търсене на Работни Места", + "JOBS_MATCHING": "Съвпадение на Работни Места", + "TEAM_TASKS": "Задачи на Екипа", + "TIME_ACTIVITY": "Време и Дейност", + "TIMESHEETS": "Табели за Времето", + "SCHEDULES": "Графици", + "EMAIL_TEMPLATES": "Шаблони за Имейл", + "REPORTS": "Отчети", + "GOALS": "Цели", + "ALL_REPORTS": "Всички Отчети", + "TIME_REPORTS": "Времеви Отчет", + "WEEKLY_TIME_REPORTS": "Седмичен Отчет", + "ACCOUNTING_REPORTS": "Счетоводни Отчети", + "PAYMENT_GATEWAYS": "Платежни Портали", + "SMS_GATEWAYS": "SMS Портали", + "CUSTOM_SMTP": "Персонализиран SMTP", + "INVENTORY": "Инвентар", + "SALES": "Продажби", + "PAYMENTS": "Плащания", + "FEATURES": "Характеристики", + "ACCOUNTING_TEMPLATES": "Счетоводни Шаблони", + "FOCUS": "Фокус", + "APPLICATIONS": "Приложения", "OPEN_GA_BROWSER": "Отвори Gauzy в браузъра", - "START_SERVER": "Стартирай сървъра", - "STOP_SERVER": "Спри сървъра" + "START_SERVER": "Стартирай Сървъра", + "STOP_SERVER": "Спри Сървъра" }, "SETTINGS_MENU": { "THEMES": "Теми", diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 2e9d344c2d4..2551ebadf71 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -99,16 +99,32 @@ const updater = new DesktopUpdater({ typeRelease: 'releases' }); +console.log('App is packaged', app.isPackaged); + +const gauzyUIPath = app.isPackaged + ? path.join(__dirname, '../data/ui/index.html') + : path.join(__dirname, './data/ui/index.html'); +console.log('Gauzy UI path', gauzyUIPath); + +const uiPath = path.join(__dirname, 'index.html'); +console.log('UI path', uiPath); + +const dirPath = app.isPackaged ? path.join(__dirname, '../data/ui') : path.join(__dirname, './data/ui'); +console.log('Dir path', dirPath); + +const timeTrackerUIPath = path.join(__dirname, 'index.html'); + const pathWindow: IPathWindow = { - gauzyUi: app.isPackaged - ? path.join(__dirname, '../data/ui/index.html') - : path.join(__dirname, './data/ui/index.html'), - ui: path.join(__dirname, 'index.html'), - dir: app.isPackaged ? path.join(__dirname, '../data/ui') : path.join(__dirname, './data/ui'), - timeTrackerUi: path.join(__dirname, 'index.html') + gauzyUi: gauzyUIPath, + ui: uiPath, + dir: dirPath, + timeTrackerUi: timeTrackerUIPath }; -const serverConfig: IServerConfig = new ServerConfig(new ReadWriteFile(pathWindow)); +const readWriteFile = new ReadWriteFile(pathWindow); + +const serverConfig: IServerConfig = new ServerConfig(readWriteFile); + const reverseProxy: ILocalServer = new ReverseProxy(serverConfig); const reverseUiProxy: ILocalServer = new ReverseUiProxy(serverConfig); @@ -259,9 +275,15 @@ const initializeConfig = async (val) => { try { serverConfig.setting = val; serverConfig.update(); + } catch (error) { + console.error('Error in initializeConfig for Server Config', error); + throw new AppError('MAINWININIT', error); + } + + try { await runMainWindow(); } catch (error) { - console.error('Error in initializeConfig', error); + console.error('Error in initializeConfig for running Main Window', error); throw new AppError('MAINWININIT', error); } }; diff --git a/apps/server/src/package.json b/apps/server/src/package.json index 2613a47c7e3..3684e2665a3 100755 --- a/apps/server/src/package.json +++ b/apps/server/src/package.json @@ -1,197 +1,201 @@ { - "name": "gauzy-server", - "productName": "Gauzy Server", - "version": "0.1.0", - "description": "Gauzy Server", - "license": "AGPL-3.0", - "homepage": "https://gauzy.co", - "repository": { - "type": "git", - "url": "https://github.com/ever-co/ever-gauzy.git" - }, - "bugs": { - "url": "https://github.com/ever-co/ever-gauzy/issues" - }, - "private": true, - "author": { - "name": "Ever Co. LTD", - "email": "ever@ever.co", - "url": "https://ever.co" - }, - "main": "index.js", - "bin": "api/main.js", - "workspaces": { - "packages": [ - "../../../packages/core", - "../../../packages/auth", - "../../../packages/desktop-window", - "../../../packages/desktop-libs", - "../../../packages/common", - "../../../packages/config", - "../../../packages/contracts", - "../../../packages/plugins/integration-ai", - "../../../packages/plugins/integration-hubstaff", - "../../../packages/plugins/integration-upwork", - "../../../packages/plugins/integration-github", - "../../../packages/plugins/integration-jira", - "../../../packages/plugin", - "../../../packages/plugins/knowledge-base", - "../../../packages/plugins/changelog", - "../../../packages/plugins/jitsu-analytics", - "../../../packages/plugins/sentry-tracing" - ] - }, - "build": { - "appId": "com.ever.gauzyserver", - "artifactName": "${name}-${version}.${ext}", - "productName": "Gauzy Server", - "copyright": "Copyright © 2019-Present. Ever Co. LTD", - "dmg": { - "sign": false - }, - "asar": true, - "npmRebuild": true, - "asarUnpack": [ - "node_modules/@sentry/electron", - "node_modules/sqlite3/lib", - "node_modules/better-sqlite3", - "node_modules/@sentry/profiling-node/lib" - ], - "directories": { - "buildResources": "icons", - "output": "../desktop-packages" - }, - "publish": [ - { - "provider": "github", - "repo": "ever-gauzy-server", - "releaseType": "release" - }, - { - "provider": "spaces", - "name": "ever", - "region": "sfo3", - "path": "/ever-gauzy-server", - "acl": "public-read" - } - ], - "mac": { - "category": "public.app-category.developer-tools", - "icon": "icon.icns", - "target": [ - "zip", - "dmg" - ], - "asarUnpack": "**/*.node", - "artifactName": "${name}-${version}.${ext}", - "hardenedRuntime": true, - "gatekeeperAssess": false, - "entitlements": "tools/build/entitlements.mas.plist", - "entitlementsInherit": "tools/build/entitlements.mas.plist" - }, - "win": { - "publisherName": "Ever", - "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - } - ], - "icon": "icon.ico", - "verifyUpdateCodeSignature": false, - "requestedExecutionLevel": "requireAdministrator" - }, - "linux": { - "icon": "linux", - "target": [ - "AppImage", - "deb", - "tar.gz" - ], - "executableName": "gauzy-server", - "artifactName": "${name}-${version}.${ext}", - "synopsis": "Server", - "category": "Development" - }, - "nsis": { - "oneClick": false, - "perMachine": true, - "createDesktopShortcut": true, - "createStartMenuShortcut": true, - "allowToChangeInstallationDirectory": true, - "allowElevation": true, - "installerIcon": "icon.ico", - "artifactName": "${name}-${version}.${ext}", - "deleteAppDataOnUninstall": true, - "menuCategory": true - }, - "extraResources": [ - "./data/**/*" - ] - }, - "dependencies": { - "@datorama/akita-ngdevtools": "^7.0.0", - "@datorama/akita": "^7.1.1", - "@electron/remote": "^2.0.8", - "@gauzy/auth": "^0.1.0", - "@gauzy/changelog-plugin": "^0.1.0", - "@gauzy/core": "^0.1.0", - "@gauzy/desktop-libs": "^0.1.0", - "@gauzy/desktop-window": "^0.1.0", - "@gauzy/jitsu-analytics-plugin": "^0.1.0", - "@gauzy/knowledge-base-plugin": "^0.1.0", - "@gauzy/sentry-plugin": "^0.1.0", - "@nestjs/platform-express": "^10.3.7", - "@sentry/electron": "^4.18.0", - "@sentry/node": "^7.101.1", - "@sentry/profiling-node": "^7.101.1", - "@sentry/replay": "^7.101.1", - "@sentry/tracing": "^7.101.1", - "@sentry/types": "^7.101.1", - "auto-launch": "5.0.5", - "consolidate": "^0.16.0", - "electron-log": "^4.4.8", - "electron-store": "^8.1.0", - "electron-updater": "^6.1.7", - "electron-util": "^0.17.2", - "ffi-napi": "^4.0.3", - "form-data": "^3.0.0", - "htmlparser2": "^8.0.2", - "iconv": "^3.0.1", - "knex": "^3.1.0", - "moment": "^2.29.4", - "node-fetch": "^2.6.7", - "node-notifier": "^8.0.0", - "node-static": "^0.7.11", - "pdfmake": "^0.2.0", - "pg-query-stream": "^4.5.4", - "pg": "^8.11.4", - "sound-play": "1.1.0", - "sqlite3": "^5.1.7", - "squirrelly": "^8.0.8", - "tslib": "^2.3.0", - "twing": "^5.0.2", - "locutus": "^2.0.30", - "underscore": "^1.13.3", - "undici": "^6.10.2" - }, - "optionalDependencies": { - "node-linux": "^0.1.12", - "node-mac": "^1.0.1", - "node-windows": "^1.0.0-beta.8" - }, - "pkg": { - "assets": [ - "api/assets/**/*", - "node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node", - "node_modules/linebreak/src/classes.trie" - ], - "targets": [ - "node16-linux-x64", - "node16-mac-x64", - "node16-win-x64" - ] - } + "name": "gauzy-server", + "productName": "Gauzy Server", + "version": "0.1.0", + "description": "Gauzy Server", + "license": "AGPL-3.0", + "homepage": "https://gauzy.co", + "repository": { + "type": "git", + "url": "https://github.com/ever-co/ever-gauzy.git" + }, + "bugs": { + "url": "https://github.com/ever-co/ever-gauzy/issues" + }, + "private": true, + "author": { + "name": "Ever Co. LTD", + "email": "ever@ever.co", + "url": "https://ever.co" + }, + "main": "index.js", + "bin": "api/main.js", + "workspaces": { + "packages": [ + "../../../packages/core", + "../../../packages/auth", + "../../../packages/desktop-window", + "../../../packages/desktop-libs", + "../../../packages/common", + "../../../packages/config", + "../../../packages/contracts", + "../../../packages/plugins/integration-ai", + "../../../packages/plugins/integration-hubstaff", + "../../../packages/plugins/integration-upwork", + "../../../packages/plugins/integration-github", + "../../../packages/plugins/integration-jira", + "../../../packages/plugin", + "../../../packages/plugins/knowledge-base", + "../../../packages/plugins/changelog", + "../../../packages/plugins/jitsu-analytics", + "../../../packages/plugins/sentry-tracing", + "../../../packages/plugins/job-search", + "../../../packages/plugins/job-proposal" + ] + }, + "build": { + "appId": "com.ever.gauzyserver", + "artifactName": "${name}-${version}.${ext}", + "productName": "Gauzy Server", + "copyright": "Copyright © 2019-Present. Ever Co. LTD", + "dmg": { + "sign": false + }, + "asar": true, + "npmRebuild": true, + "asarUnpack": [ + "node_modules/@sentry/electron", + "node_modules/sqlite3/lib", + "node_modules/better-sqlite3", + "node_modules/@sentry/profiling-node/lib" + ], + "directories": { + "buildResources": "icons", + "output": "../desktop-packages" + }, + "publish": [ + { + "provider": "github", + "repo": "ever-gauzy-server", + "releaseType": "release" + }, + { + "provider": "spaces", + "name": "ever", + "region": "sfo3", + "path": "/ever-gauzy-server", + "acl": "public-read" + } + ], + "mac": { + "category": "public.app-category.developer-tools", + "icon": "icon.icns", + "target": [ + "zip", + "dmg" + ], + "asarUnpack": "**/*.node", + "artifactName": "${name}-${version}.${ext}", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "tools/build/entitlements.mas.plist", + "entitlementsInherit": "tools/build/entitlements.mas.plist" + }, + "win": { + "publisherName": "Ever", + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + } + ], + "icon": "icon.ico", + "verifyUpdateCodeSignature": false, + "requestedExecutionLevel": "requireAdministrator" + }, + "linux": { + "icon": "linux", + "target": [ + "AppImage", + "deb", + "tar.gz" + ], + "executableName": "gauzy-server", + "artifactName": "${name}-${version}.${ext}", + "synopsis": "Server", + "category": "Development" + }, + "nsis": { + "oneClick": false, + "perMachine": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "allowToChangeInstallationDirectory": true, + "allowElevation": true, + "installerIcon": "icon.ico", + "artifactName": "${name}-${version}.${ext}", + "deleteAppDataOnUninstall": true, + "menuCategory": true + }, + "extraResources": [ + "./data/**/*" + ] + }, + "dependencies": { + "@datorama/akita-ngdevtools": "^7.0.0", + "@datorama/akita": "^7.1.1", + "@electron/remote": "^2.0.8", + "@gauzy/auth": "^0.1.0", + "@gauzy/changelog-plugin": "^0.1.0", + "@gauzy/core": "^0.1.0", + "@gauzy/desktop-libs": "^0.1.0", + "@gauzy/desktop-window": "^0.1.0", + "@gauzy/jitsu-analytics-plugin": "^0.1.0", + "@gauzy/job-proposal-plugin": "^0.1.0", + "@gauzy/job-search-plugin": "^0.1.0", + "@gauzy/knowledge-base-plugin": "^0.1.0", + "@gauzy/sentry-plugin": "^0.1.0", + "@nestjs/platform-express": "^10.3.7", + "@sentry/electron": "^4.18.0", + "@sentry/node": "^7.101.1", + "@sentry/profiling-node": "^7.101.1", + "@sentry/replay": "^7.101.1", + "@sentry/tracing": "^7.101.1", + "@sentry/types": "^7.101.1", + "auto-launch": "5.0.5", + "consolidate": "^0.16.0", + "electron-log": "^4.4.8", + "electron-store": "^8.1.0", + "electron-updater": "^6.1.7", + "electron-util": "^0.17.2", + "ffi-napi": "^4.0.3", + "form-data": "^3.0.0", + "htmlparser2": "^8.0.2", + "iconv": "^3.0.1", + "knex": "^3.1.0", + "locutus": "^2.0.30", + "moment": "^2.29.4", + "node-fetch": "^2.6.7", + "node-notifier": "^8.0.0", + "node-static": "^0.7.11", + "pdfmake": "^0.2.0", + "pg-query-stream": "^4.5.4", + "pg": "^8.11.4", + "sound-play": "1.1.0", + "sqlite3": "^5.1.7", + "squirrelly": "^8.0.8", + "tslib": "^2.3.0", + "twing": "^5.0.2", + "underscore": "^1.13.3", + "undici": "^6.10.2" + }, + "optionalDependencies": { + "node-linux": "^0.1.12", + "node-mac": "^1.0.1", + "node-windows": "^1.0.0-beta.8" + }, + "pkg": { + "assets": [ + "api/assets/**/*", + "node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node", + "node_modules/linebreak/src/classes.trie" + ], + "targets": [ + "node16-linux-x64", + "node16-mac-x64", + "node16-win-x64" + ] + } } diff --git a/docker-compose.build.yml b/docker-compose.build.yml index 83d2ff846d2..22e4dd26bef 100644 --- a/docker-compose.build.yml +++ b/docker-compose.build.yml @@ -1,226 +1,7 @@ -version: '3.8' +include: + - ./docker-compose.infra.yml services: - db: - image: postgres:15-alpine - container_name: db - restart: always - environment: - POSTGRES_DB: ${DB_NAME:-gauzy} - POSTGRES_USER: ${DB_USER:-postgres} - POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} - healthcheck: - test: - [ - 'CMD-SHELL', - 'psql postgres://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@localhost:5432/$${POSTGRES_DB} || exit 1' - ] - volumes: - - postgres_data:/var/lib/postgresql/data/ - - ./.deploy/db/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh - ports: - - '5432:5432' - networks: - - overlay - - zipkin: - image: ghcr.io/openzipkin/zipkin-slim:latest - container_name: zipkin - # Environment settings are defined here https://github.com/openzipkin/zipkin/blob/master/zipkin-server/README.md#environment-variables - environment: - - STORAGE_TYPE=mem - # Uncomment to enable self-tracing - # - SELF_TRACING_ENABLED=true - # Uncomment to increase heap size - # - JAVA_OPTS=-Xms128m -Xmx128m -XX:+ExitOnOutOfMemoryError - ports: - # Port used for the Zipkin UI and HTTP Api - - 9411:9411 - networks: - - overlay - - cube: - image: cubejs/cube:latest - container_name: cube - ports: - - '4000:4000' # Cube Playground - - '5430:5430' # Port for Cube SQL - environment: - CUBEJS_DEV_MODE: 'true' - CUBEJS_DB_TYPE: postgres - CUBEJS_DB_HOST: db - CUBEJS_DB_PORT: 5432 - CUBEJS_DB_NAME: ${DB_NAME:-gauzy} - CUBEJS_DB_USER: ${DB_USER:-postgres} - CUBEJS_DB_PASS: ${DB_PASS:-gauzy_password} - # Credentials to connect to Cube SQL APIs - CUBEJS_PG_SQL_PORT: 5430 - CUBEJS_SQL_USER: ${CUBE_USER:-cube_user} - CUBEJS_SQL_PASSWORD: ${CUBE_PASS:-cube_pass} - volumes: - - 'cube_data:/cube/conf' - links: - - db - networks: - - overlay - - jitsu: - container_name: jitsu - image: jitsucom/jitsu:latest - extra_hosts: - - 'host.docker.internal:host-gateway' - environment: - - REDIS_URL=redis://redis:6379 - # Retroactive users recognition can affect RAM significant. - # Read more about the solution https://jitsu.com/docs/other-features/retroactive-user-recognition - - USER_RECOGNITION_ENABLED=true - - USER_RECOGNITION_REDIS_URL=redis://jitsu_redis_users_recognition:6380 - - TERM=xterm-256color - depends_on: - redis: - condition: service_healthy - jitsu_redis_users_recognition: - condition: service_healthy - volumes: - - ./.deploy/jitsu/configurator/data/logs:/home/configurator/data/logs - - ./.deploy/jitsu/server/data/logs:/home/eventnative/data/logs - - ./.deploy/jitsu/server/data/logs/events:/home/eventnative/data/logs/events - - /var/run/docker.sock:/var/run/docker.sock - - jitsu_workspace:/home/eventnative/data/airbyte - restart: always - ports: - - '8000:8000' - networks: - - overlay - - elasticsearch: - image: 'elasticsearch:7.17.7' - container_name: elasticsearch - volumes: - - elasticsearch_data:/usr/share/elasticsearch/data - environment: - ES_JAVA_OPTS: -Xms512m -Xmx1024m - discovery.type: single-node - http.port: 9200 - http.cors.enabled: 'true' - http.cors.allow-origin: http://localhost:3000,http://127.0.0.1:3000,http://localhost:1358,http://127.0.0.1:1358 - http.cors.allow-headers: X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization - http.cors.allow-credentials: 'true' - bootstrap.memory_lock: 'true' - xpack.security.enabled: 'false' - ports: - - '9200' - - '9300' - ulimits: - memlock: - soft: -1 - hard: -1 - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:9200/_cat/health'] - interval: 5s - timeout: 5s - retries: 10 - start_period: 20s - networks: - - overlay - - # Elasticsearch Management UI - dejavu: - image: appbaseio/dejavu:3.6.0 - container_name: dejavu - ports: - - '1358:1358' - links: - - elasticsearch - networks: - - overlay - - # TODO: For now used in Jitsu, but we will need to create another one dedicated for Jitsu later - redis: - image: 'redis:7.0.2-alpine' - container_name: redis - restart: unless-stopped - healthcheck: - test: ['CMD-SHELL', 'redis-cli -h localhost -p 6379 PING'] - interval: 1s - timeout: 30s - ports: - - '6379' - volumes: - - ./.deploy/redis/data:/data - networks: - - overlay - - jitsu_redis_users_recognition: - image: 'redis:7.0.2-alpine' - container_name: jitsu_redis_users_recognition - command: redis-server /usr/local/etc/redis/redis.conf - restart: unless-stopped - healthcheck: - test: ['CMD-SHELL', 'redis-cli -h localhost -p 6380 PING'] - interval: 1s - timeout: 30s - ports: - - '6380' - volumes: - - ./.deploy/redis/jitsu_users_recognition/data:/data - - ./.deploy/redis/jitsu_users_recognition/redis.conf:/usr/local/etc/redis/redis.conf - networks: - - overlay - - minio: - restart: unless-stopped - image: quay.io/minio/minio:latest - container_name: minio - volumes: - - minio_data:/data - environment: - MINIO_ROOT_USER: ever-gauzy-access-key - MINIO_ROOT_PASSWORD: ever-gauzy-secret-key - command: server /data --address :9000 --console-address ":9001" - ports: - - 9000:9000 - - 9001:9001 - networks: - - overlay - - minio_create_buckets: - image: minio/mc - environment: - MINIO_ROOT_USER: ever-gauzy-access-key - MINIO_ROOT_PASSWORD: ever-gauzy-secret-key - entrypoint: - - '/bin/sh' - - '-c' - command: - - "until (/usr/bin/mc alias set minio http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD) do - echo 'Waiting to start minio...' && sleep 1; - done; - /usr/bin/mc mb minio/ever-gauzy --region=eu-north-1; - exit 0;" - depends_on: - - minio - networks: - - overlay - - pgweb: - image: sosedoff/pgweb - container_name: pgweb - restart: always - depends_on: - - db - links: - - db:${DB_HOST:-db} - environment: - POSTGRES_DB: ${DB_NAME:-gauzy} - POSTGRES_USER: ${DB_USER:-postgres} - POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} - PGWEB_DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:-gauzy_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-gauzy}?sslmode=disable - ports: - - '8081:8081' - networks: - - overlay - api: container_name: api image: gauzy-api:latest @@ -278,7 +59,7 @@ services: depends_on: db: condition: service_healthy - jaeger: + zipkin: condition: service_started redis: condition: service_started @@ -393,18 +174,3 @@ services: - '4200:${UI_PORT:-4200}' networks: - overlay - -volumes: - # webapp_node_modules: - # api_node_modules: - redis_data: {} - postgres_data: {} - elasticsearch_data: {} - minio_data: {} - cube_data: {} - certificates: {} - jitsu_workspace: {} - -networks: - overlay: - driver: bridge diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml index c9c1f43480e..3a5d6001d16 100644 --- a/docker-compose.demo.yml +++ b/docker-compose.demo.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: db: image: postgres:15-alpine diff --git a/docker-compose.infra.yml b/docker-compose.infra.yml new file mode 100644 index 00000000000..0aae0b4ad4f --- /dev/null +++ b/docker-compose.infra.yml @@ -0,0 +1,233 @@ +services: + db: + image: postgres:15-alpine + container_name: db + restart: always + environment: + POSTGRES_DB: ${DB_NAME:-gauzy} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} + healthcheck: + test: + [ + 'CMD-SHELL', + 'psql postgres://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@localhost:5432/$${POSTGRES_DB} || exit 1' + ] + volumes: + - postgres_data:/var/lib/postgresql/data/ + - ./.deploy/db/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh + ports: + - '5432:5432' + networks: + - overlay + + zipkin: + image: ghcr.io/openzipkin/zipkin-slim:latest + container_name: zipkin + # Environment settings are defined here https://github.com/openzipkin/zipkin/blob/master/zipkin-server/README.md#environment-variables + environment: + - STORAGE_TYPE=mem + # Uncomment to enable self-tracing + # - SELF_TRACING_ENABLED=true + # Uncomment to increase heap size + # - JAVA_OPTS=-Xms128m -Xmx128m -XX:+ExitOnOutOfMemoryError + ports: + # Port used for the Zipkin UI and HTTP Api + - 9411:9411 + networks: + - overlay + + cube: + image: cubejs/cube:latest + container_name: cube + ports: + - '4000:4000' # Cube Playground + - '5430:5430' # Port for Cube SQL + environment: + CUBEJS_DEV_MODE: 'true' + CUBEJS_DB_TYPE: postgres + CUBEJS_DB_HOST: db + CUBEJS_DB_PORT: 5432 + CUBEJS_DB_NAME: ${DB_NAME:-gauzy} + CUBEJS_DB_USER: ${DB_USER:-postgres} + CUBEJS_DB_PASS: ${DB_PASS:-gauzy_password} + # Credentials to connect to Cube SQL APIs + CUBEJS_PG_SQL_PORT: 5430 + CUBEJS_SQL_USER: ${CUBE_USER:-cube_user} + CUBEJS_SQL_PASSWORD: ${CUBE_PASS:-cube_pass} + volumes: + - 'cube_data:/cube/conf' + links: + - db + networks: + - overlay + + jitsu: + container_name: jitsu + image: jitsucom/jitsu:latest + extra_hosts: + - 'host.docker.internal:host-gateway' + environment: + - REDIS_URL=redis://redis:6379 + # Retroactive users recognition can affect RAM significant. + # Read more about the solution https://jitsu.com/docs/other-features/retroactive-user-recognition + - USER_RECOGNITION_ENABLED=true + - USER_RECOGNITION_REDIS_URL=redis://jitsu_redis_users_recognition:6380 + - TERM=xterm-256color + depends_on: + redis: + condition: service_healthy + jitsu_redis_users_recognition: + condition: service_healthy + volumes: + - ./.deploy/jitsu/configurator/data/logs:/home/configurator/data/logs + - ./.deploy/jitsu/server/data/logs:/home/eventnative/data/logs + - ./.deploy/jitsu/server/data/logs/events:/home/eventnative/data/logs/events + - /var/run/docker.sock:/var/run/docker.sock + - jitsu_workspace:/home/eventnative/data/airbyte + restart: always + ports: + - '8000:8000' + networks: + - overlay + + elasticsearch: + image: 'elasticsearch:7.17.7' + container_name: elasticsearch + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + environment: + ES_JAVA_OPTS: -Xms512m -Xmx1024m + discovery.type: single-node + http.port: 9200 + http.cors.enabled: 'true' + http.cors.allow-origin: http://localhost:3000,http://127.0.0.1:3000,http://localhost:1358,http://127.0.0.1:1358 + http.cors.allow-headers: X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization + http.cors.allow-credentials: 'true' + bootstrap.memory_lock: 'true' + xpack.security.enabled: 'false' + ports: + - '9200' + - '9300' + ulimits: + memlock: + soft: -1 + hard: -1 + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9200/_cat/health'] + interval: 5s + timeout: 5s + retries: 10 + start_period: 20s + networks: + - overlay + + # Elasticsearch Management UI + dejavu: + image: appbaseio/dejavu:3.6.0 + container_name: dejavu + ports: + - '1358:1358' + links: + - elasticsearch + networks: + - overlay + + # TODO: For now used in Jitsu, but we will need to create another one dedicated for Jitsu later + redis: + image: 'redis:7.0.2-alpine' + container_name: redis + restart: unless-stopped + healthcheck: + test: ['CMD-SHELL', 'redis-cli -h localhost -p 6379 PING'] + interval: 1s + timeout: 30s + ports: + - '6379' + volumes: + - ./.deploy/redis/data:/data + networks: + - overlay + + jitsu_redis_users_recognition: + image: 'redis:7.0.2-alpine' + container_name: jitsu_redis_users_recognition + command: redis-server /usr/local/etc/redis/redis.conf + restart: unless-stopped + healthcheck: + test: ['CMD-SHELL', 'redis-cli -h localhost -p 6380 PING'] + interval: 1s + timeout: 30s + ports: + - '6380' + volumes: + - ./.deploy/redis/jitsu_users_recognition/data:/data + - ./.deploy/redis/jitsu_users_recognition/redis.conf:/usr/local/etc/redis/redis.conf + networks: + - overlay + + minio: + restart: unless-stopped + image: quay.io/minio/minio:latest + container_name: minio + volumes: + - minio_data:/data + environment: + MINIO_ROOT_USER: ever-gauzy-access-key + MINIO_ROOT_PASSWORD: ever-gauzy-secret-key + command: server /data --address :9000 --console-address ":9001" + ports: + - 9000:9000 + - 9001:9001 + networks: + - overlay + + minio_create_buckets: + image: minio/mc + environment: + MINIO_ROOT_USER: ever-gauzy-access-key + MINIO_ROOT_PASSWORD: ever-gauzy-secret-key + entrypoint: + - '/bin/sh' + - '-c' + command: + - "until (/usr/bin/mc alias set minio http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD) do + echo 'Waiting to start minio...' && sleep 1; + done; + /usr/bin/mc mb minio/ever-gauzy --region=eu-north-1; + exit 0;" + depends_on: + - minio + networks: + - overlay + + pgweb: + image: sosedoff/pgweb + container_name: pgweb + restart: always + depends_on: + - db + links: + - db:${DB_HOST:-db} + environment: + POSTGRES_DB: ${DB_NAME:-gauzy} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} + PGWEB_DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:-gauzy_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-gauzy}?sslmode=disable + ports: + - '8081:8081' + networks: + - overlay + +volumes: + postgres_data: {} + redis_data: {} + elasticsearch_data: {} + minio_data: {} + cube_data: {} + certificates: {} + jitsu_workspace: {} + +networks: + overlay: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 36d1e247bf4..c6accade5bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,226 +1,7 @@ -version: '3.8' +include: + - ./docker-compose.infra.yml services: - db: - image: postgres:15-alpine - container_name: db - restart: always - environment: - POSTGRES_DB: ${DB_NAME:-gauzy} - POSTGRES_USER: ${DB_USER:-postgres} - POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} - healthcheck: - test: - [ - 'CMD-SHELL', - 'psql postgres://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@localhost:5432/$${POSTGRES_DB} || exit 1' - ] - volumes: - - postgres_data:/var/lib/postgresql/data/ - - ./.deploy/db/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh - ports: - - '5432:5432' - networks: - - overlay - - zipkin: - image: ghcr.io/openzipkin/zipkin-slim:latest - container_name: zipkin - # Environment settings are defined here https://github.com/openzipkin/zipkin/blob/master/zipkin-server/README.md#environment-variables - environment: - - STORAGE_TYPE=mem - # Uncomment to enable self-tracing - # - SELF_TRACING_ENABLED=true - # Uncomment to increase heap size - # - JAVA_OPTS=-Xms128m -Xmx128m -XX:+ExitOnOutOfMemoryError - ports: - # Port used for the Zipkin UI and HTTP Api - - 9411:9411 - networks: - - overlay - - cube: - image: cubejs/cube:latest - container_name: cube - ports: - - '4000:4000' # Cube Playground - - '5430:5430' # Port for Cube SQL - environment: - CUBEJS_DEV_MODE: 'true' - CUBEJS_DB_TYPE: postgres - CUBEJS_DB_HOST: db - CUBEJS_DB_PORT: 5432 - CUBEJS_DB_NAME: ${DB_NAME:-gauzy} - CUBEJS_DB_USER: ${DB_USER:-postgres} - CUBEJS_DB_PASS: ${DB_PASS:-gauzy_password} - # Credentials to connect to Cube SQL APIs - CUBEJS_PG_SQL_PORT: 5430 - CUBEJS_SQL_USER: ${CUBE_USER:-cube_user} - CUBEJS_SQL_PASSWORD: ${CUBE_PASS:-cube_pass} - volumes: - - 'cube_data:/cube/conf' - links: - - db - networks: - - overlay - - jitsu: - container_name: jitsu - image: jitsucom/jitsu:latest - extra_hosts: - - 'host.docker.internal:host-gateway' - environment: - - REDIS_URL=redis://redis:6379 - # Retroactive users recognition can affect RAM significant. - # Read more about the solution https://jitsu.com/docs/other-features/retroactive-user-recognition - - USER_RECOGNITION_ENABLED=true - - USER_RECOGNITION_REDIS_URL=redis://jitsu_redis_users_recognition:6380 - - TERM=xterm-256color - depends_on: - redis: - condition: service_healthy - jitsu_redis_users_recognition: - condition: service_healthy - volumes: - - ./.deploy/jitsu/configurator/data/logs:/home/configurator/data/logs - - ./.deploy/jitsu/server/data/logs:/home/eventnative/data/logs - - ./.deploy/jitsu/server/data/logs/events:/home/eventnative/data/logs/events - - /var/run/docker.sock:/var/run/docker.sock - - jitsu_workspace:/home/eventnative/data/airbyte - restart: always - ports: - - '8000:8000' - networks: - - overlay - - elasticsearch: - image: 'elasticsearch:7.17.7' - container_name: elasticsearch - volumes: - - elasticsearch_data:/usr/share/elasticsearch/data - environment: - ES_JAVA_OPTS: -Xms512m -Xmx1024m - discovery.type: single-node - http.port: 9200 - http.cors.enabled: 'true' - http.cors.allow-origin: http://localhost:3000,http://127.0.0.1:3000,http://localhost:1358,http://127.0.0.1:1358 - http.cors.allow-headers: X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization - http.cors.allow-credentials: 'true' - bootstrap.memory_lock: 'true' - xpack.security.enabled: 'false' - ports: - - '9200' - - '9300' - ulimits: - memlock: - soft: -1 - hard: -1 - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:9200/_cat/health'] - interval: 5s - timeout: 5s - retries: 10 - start_period: 20s - networks: - - overlay - - # Elasticsearch Management UI - dejavu: - image: appbaseio/dejavu:3.6.0 - container_name: dejavu - ports: - - '1358:1358' - links: - - elasticsearch - networks: - - overlay - - # TODO: For now used in Jitsu, but we will need to create another one dedicated for Jitsu later - redis: - image: 'redis:7.0.2-alpine' - container_name: redis - restart: unless-stopped - healthcheck: - test: ['CMD-SHELL', 'redis-cli -h localhost -p 6379 PING'] - interval: 1s - timeout: 30s - ports: - - '6379' - volumes: - - ./.deploy/redis/data:/data - networks: - - overlay - - jitsu_redis_users_recognition: - image: 'redis:7.0.2-alpine' - container_name: jitsu_redis_users_recognition - command: redis-server /usr/local/etc/redis/redis.conf - restart: unless-stopped - healthcheck: - test: ['CMD-SHELL', 'redis-cli -h localhost -p 6380 PING'] - interval: 1s - timeout: 30s - ports: - - '6380' - volumes: - - ./.deploy/redis/jitsu_users_recognition/data:/data - - ./.deploy/redis/jitsu_users_recognition/redis.conf:/usr/local/etc/redis/redis.conf - networks: - - overlay - - minio: - restart: unless-stopped - image: quay.io/minio/minio:latest - container_name: minio - volumes: - - minio_data:/data - environment: - MINIO_ROOT_USER: ever-gauzy-access-key - MINIO_ROOT_PASSWORD: ever-gauzy-secret-key - command: server /data --address :9000 --console-address ":9001" - ports: - - 9000:9000 - - 9001:9001 - networks: - - overlay - - minio_create_buckets: - image: minio/mc - environment: - MINIO_ROOT_USER: ever-gauzy-access-key - MINIO_ROOT_PASSWORD: ever-gauzy-secret-key - entrypoint: - - '/bin/sh' - - '-c' - command: - - "until (/usr/bin/mc alias set minio http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD) do - echo 'Waiting to start minio...' && sleep 1; - done; - /usr/bin/mc mb minio/ever-gauzy --region=eu-north-1; - exit 0;" - depends_on: - - minio - networks: - - overlay - - pgweb: - image: sosedoff/pgweb - container_name: pgweb - restart: always - depends_on: - - db - links: - - db:${DB_HOST:-db} - environment: - POSTGRES_DB: ${DB_NAME:-gauzy} - POSTGRES_USER: ${DB_USER:-postgres} - POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} - PGWEB_DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:-gauzy_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-gauzy}?sslmode=disable - ports: - - '8081:8081' - networks: - - overlay - api: container_name: api image: ghcr.io/ever-co/gauzy-api:latest @@ -271,7 +52,7 @@ services: depends_on: db: condition: service_healthy - jaeger: + zipkin: condition: service_started redis: condition: service_started @@ -358,18 +139,3 @@ services: - '4200:${UI_PORT:-4200}' networks: - overlay - -volumes: - # webapp_node_modules: - # api_node_modules: - redis_data: {} - postgres_data: {} - elasticsearch_data: {} - minio_data: {} - cube_data: {} - certificates: {} - jitsu_workspace: {} - -networks: - overlay: - driver: bridge diff --git a/package.json b/package.json index 18fcdfa9c7b..9077ac10635 100644 --- a/package.json +++ b/package.json @@ -133,8 +133,8 @@ "build:package:plugin:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugin build", "build:package:plugins:pre": "yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:integration-jira", "build:package:plugins:pre:prod": "yarn run build:package:plugin:integration-ai:prod && yarn run build:package:plugin:integration-hubstaff:prod && yarn run build:package:plugin:integration-upwork:prod && yarn run build:package:plugin:integration-github:prod && yarn run build:package:plugin:integration-jira:prod", - "build:package:plugins:post": "yarn run build:package:plugin:sentry && yarn run build:package:plugin:jitsu-analytic && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog", - "build:package:plugins:post:prod": "yarn run build:package:plugin:sentry:prod && yarn run build:package:plugin:jitsu-analytic:prod && yarn run build:package:plugin:product-reviews:prod && yarn run build:package:plugin:knowledge-base:prod && yarn run build:package:plugin:changelog:prod", + "build:package:plugins:post": "yarn run build:package:plugin:sentry && yarn run build:package:plugin:jitsu-analytic && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:job-search && yarn run build:package:plugin:job-proposal && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog", + "build:package:plugins:post:prod": "yarn run build:package:plugin:sentry:prod && yarn run build:package:plugin:jitsu-analytic:prod && yarn run build:package:plugin:product-reviews:prod && yarn run build:package:plugin:job-search:prod && yarn run build:package:plugin:knowledge-base:prod && yarn run build:package:plugin:changelog:prod", "build:package:plugin:integration-ai": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-ai build", "build:package:plugin:integration-ai:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-ai build", "build:package:plugin:integration-hubstaff": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-hubstaff build", @@ -151,6 +151,10 @@ "build:package:plugin:jitsu-analytic:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/jitsu-analytics build", "build:package:plugin:product-reviews": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/product-reviews build", "build:package:plugin:product-reviews:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/product-reviews build", + "build:package:plugin:job-search": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-search build", + "build:package:plugin:job-search:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-search build", + "build:package:plugin:job-proposal": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-proposal build", + "build:package:plugin:job-proposal:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-proposal build", "build:package:plugin:knowledge-base": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/knowledge-base build", "build:package:plugin:knowledge-base:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/knowledge-base build", "build:package:plugin:changelog": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/changelog build", diff --git a/packages/common/src/custom-embedded-field-types.ts b/packages/common/src/custom-embedded-field-types.ts new file mode 100644 index 00000000000..158b87d5b8d --- /dev/null +++ b/packages/common/src/custom-embedded-field-types.ts @@ -0,0 +1,36 @@ +/** + * Configuration for a custom embedded field within a relation. + */ +export type RelationCustomEmbeddedFieldConfig = { + /** Name of the custom field. */ + propertyPath: string; + /** Target entity for the relation. */ + entity: T; + /** Type of the relation field. */ + type: string; + /** Name of the relation. */ + relationType: string; + /** Indicates if the relation should be eagerly loaded. */ + eager?: boolean; + /** Indicates if the relation is nullable. */ + nullable?: boolean; + /** Indicates if the relation should have a unique constraint. */ + unique?: boolean; + /** Specifies the inverse side of the relation. */ + inverseSide?: string | ((object: T) => any); +}; + +/** + * Alias for RelationCustomEmbeddedFieldConfig. + */ +export type CustomEmbeddedFieldConfig = RelationCustomEmbeddedFieldConfig; + +/** + * Defines custom embedded fields for different entities. + */ +export interface CustomEmbeddedFields { + /** Custom fields for the Tag entity. */ + Tag?: CustomEmbeddedFieldConfig[]; + /** Custom fields for the Employee entity. */ + Employee?: CustomEmbeddedFieldConfig[]; +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 57bb44d31b1..bae72d1bf3c 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -5,3 +5,4 @@ export * from './guards'; export * from './interfaces'; export * from './utils'; export * from './shared-types'; +export * from './custom-embedded-field-types'; diff --git a/packages/common/src/interfaces/IApplicationPluginConfig.ts b/packages/common/src/interfaces/IApplicationPluginConfig.ts index 0fc8c71ed7b..3ef0bab1d48 100644 --- a/packages/common/src/interfaces/IApplicationPluginConfig.ts +++ b/packages/common/src/interfaces/IApplicationPluginConfig.ts @@ -4,6 +4,7 @@ import { MikroOrmModuleOptions } from '@mikro-orm/nestjs'; import { KnexModuleOptions } from 'nest-knexjs'; import { PluginDefinition } from 'apollo-server-core'; import { AbstractLogger } from './IAbstractLogger'; +import { CustomEmbeddedFields } from '../custom-embedded-field-types'; /** * Configuration options for GraphQL. @@ -150,6 +151,12 @@ export interface ApplicationPluginConfig { */ logger?: AbstractLogger; + /** + * Custom fields configuration. + * Defines custom fields for different entities in the application. + */ + customFields?: CustomEmbeddedFields; + /** * Authentication options. * @description Defines options for configuring authentication in the application. diff --git a/packages/common/src/shared-types.ts b/packages/common/src/shared-types.ts index 8c889c551af..37b13d97137 100644 --- a/packages/common/src/shared-types.ts +++ b/packages/common/src/shared-types.ts @@ -10,9 +10,23 @@ export type DeepPartial = { }; /** - * Represents a constructor function type. + * Represents a constructor function or class type. * @template T - Type to be instantiated. */ -export interface ConstructorType extends Function { +export interface Type extends Function { + /** + * Constructor signature. + * Creates a new instance of type T with the provided arguments. + * @param {...any[]} args - Arguments to be passed to the constructor. + * @returns {T} - An instance of type T. + */ new(...args: any[]): T; } + +/** + * Represents an object with custom fields. + * @template T - Type of the custom fields. + */ +export interface CustomFieldsObject { + [key: string]: T; +} diff --git a/packages/config/src/default-configuration.ts b/packages/config/src/default-configuration.ts index aa4697447db..97965ae66ac 100644 --- a/packages/config/src/default-configuration.ts +++ b/packages/config/src/default-configuration.ts @@ -65,6 +65,10 @@ export const defaultConfiguration: ApplicationPluginConfig = { ...dbKnexConnectionConfig }, plugins: [], + customFields: { + Tag: [], + Employee: [] + }, authOptions: { expressSessionSecret: process.env.EXPRESS_SESSION_SECRET || 'gauzy', userPasswordBcryptSaltRounds: 12, diff --git a/packages/core/package.json b/packages/core/package.json index 0d8c9fbcddd..760bb955724 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -59,14 +59,14 @@ "@grpc/grpc-js": "^1.7.3", "@honeycombio/opentelemetry-node": "0.6.1", "@jitsu/js": "^1.8.2", - "@mikro-orm/better-sqlite": "^6.1.12", - "@mikro-orm/core": "^6.1.12", - "@mikro-orm/knex": "^6.1.12", - "@mikro-orm/mongodb": "^6.1.12", - "@mikro-orm/mysql": "^6.1.12", + "@mikro-orm/better-sqlite": "^6.2.1", + "@mikro-orm/core": "^6.2.1", + "@mikro-orm/knex": "^6.2.1", + "@mikro-orm/mongodb": "^6.2.1", + "@mikro-orm/mysql": "^6.2.1", "@mikro-orm/nestjs": "^5.2.3", - "@mikro-orm/postgresql": "^6.1.12", - "@mikro-orm/sqlite": "^6.1.12", + "@mikro-orm/postgresql": "^6.2.1", + "@mikro-orm/sqlite": "^6.2.1", "@nestjs/apollo": "^12.1.0", "@nestjs/axios": "^3.0.2", "@nestjs/cache-manager": "^2.2.1", @@ -153,7 +153,6 @@ "graphql-tools": "^8.2.0", "handlebars": "^4.7.6", "helmet": "^4.1.1", - "html-to-text": "^9.0.5", "image-size": "^1.0.2", "jimp": "^0.22.7", "jsonwebtoken": "^9.0.0", @@ -170,6 +169,7 @@ "mysql2": "^3.9.3", "nats": "^2.6.1", "nest-knexjs": "^0.0.21", + "nestjs-cls": "^4.3.0", "nestjs-i18n": "^10.4.0", "node-fetch": "^2.6.7", "nodemailer": "^6.4.11", @@ -197,7 +197,6 @@ "upwork-api": "^1.3.8", "uuid": "^8.3.0", "web-push": "^3.4.4", - "nestjs-cls": "^4.3.0", "yargs": "^17.5.0" }, "devDependencies": { diff --git a/packages/core/src/app.module.ts b/packages/core/src/app.module.ts index d1ada5b4bc9..da147bfd14e 100644 --- a/packages/core/src/app.module.ts +++ b/packages/core/src/app.module.ts @@ -35,7 +35,6 @@ import { OrganizationModule } from './organization/organization.module'; import { IncomeModule } from './income/income.module'; import { ExpenseModule } from './expense/expense.module'; import { EmployeeSettingModule } from './employee-setting/employee-setting.module'; -import { EmployeeJobPostModule } from './employee-job/employee-job.module'; import { EmployeeAppointmentModule } from './employee-appointment/employee-appointment.module'; import { AuthModule } from './auth/auth.module'; import { UserOrganizationModule } from './user-organization/user-organization.module'; @@ -53,7 +52,6 @@ import { OrganizationTeamJoinRequestModule } from './organization-team-join-requ import { OrganizationAwardModule } from './organization-award/organization-award.module'; import { OrganizationLanguageModule } from './organization-language/organization-language.module'; import { OrganizationDocumentModule } from './organization-document/organization-document.module'; -import { ProposalModule } from './proposal/proposal.module'; import { CountryModule } from './country/country.module'; import { CurrencyModule } from './currency/currency.module'; import { InviteModule } from './invite/invite.module'; @@ -125,9 +123,7 @@ import { EmployeeAwardModule } from './employee-award/employee-award.module'; import { InvoiceEstimateHistoryModule } from './invoice-estimate-history/invoice-estimate-history.module'; import { GoalKpiTemplateModule } from './goal-kpi-template/goal-kpi-template.module'; import { TenantSettingModule } from './tenant/tenant-setting/tenant-setting.module'; -import { EmployeeJobPresetModule } from './employee-job-preset/employee-job-preset.module'; import { ReportModule } from './reports/report.module'; -import { EmployeeProposalTemplateModule } from './employee-proposal-template/employee-proposal-template.module'; import { CustomSmtpModule } from './custom-smtp/custom-smtp.module'; import { FeatureModule } from './feature/feature.module'; import { ImageAssetModule } from './image-asset/image-asset.module'; @@ -203,84 +199,84 @@ if (environment.THROTTLE_ENABLED) { }), ...(process.env.REDIS_ENABLED === 'true' ? [ - CacheModule.registerAsync({ - isGlobal: true, - useFactory: async () => { - const url = - process.env.REDIS_URL || - (process.env.REDIS_TLS === 'true' - ? `rediss://${process.env.REDIS_USER}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}` - : `redis://${process.env.REDIS_USER}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`); + CacheModule.registerAsync({ + isGlobal: true, + useFactory: async () => { + const url = + process.env.REDIS_URL || + (process.env.REDIS_TLS === 'true' + ? `rediss://${process.env.REDIS_USER}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}` + : `redis://${process.env.REDIS_USER}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`); - console.log('REDIS_URL: ', url); + console.log('REDIS_URL: ', url); - let host, port, username, password; + let host, port, username, password; - const isTls = url.startsWith('rediss://'); + const isTls = url.startsWith('rediss://'); - // Removing the protocol part - let authPart = url.split('://')[1]; + // Removing the protocol part + let authPart = url.split('://')[1]; - // Check if the URL contains '@' (indicating the presence of username/password) - if (authPart.includes('@')) { - // Splitting user:password and host:port - let [userPass, hostPort] = authPart.split('@'); - [username, password] = userPass.split(':'); - [host, port] = hostPort.split(':'); - } else { - // If there is no '@', it means there is no username/password - [host, port] = authPart.split(':'); - } + // Check if the URL contains '@' (indicating the presence of username/password) + if (authPart.includes('@')) { + // Splitting user:password and host:port + let [userPass, hostPort] = authPart.split('@'); + [username, password] = userPass.split(':'); + [host, port] = hostPort.split(':'); + } else { + // If there is no '@', it means there is no username/password + [host, port] = authPart.split(':'); + } - port = parseInt(port); + port = parseInt(port); - const storeOptions = { - url: url, - username: username, - password: password, - isolationPoolOptions: { - min: 1, - max: 100 - }, - socket: { - tls: isTls, - host: host, - port: port, - passphrase: password, - rejectUnauthorized: process.env.NODE_ENV === 'production' - }, - ttl: 60 * 60 * 24 * 7 // 1 week, - }; + const storeOptions = { + url: url, + username: username, + password: password, + isolationPoolOptions: { + min: 1, + max: 100 + }, + socket: { + tls: isTls, + host: host, + port: port, + passphrase: password, + rejectUnauthorized: process.env.NODE_ENV === 'production' + }, + ttl: 60 * 60 * 24 * 7 // 1 week, + }; - const store = await redisStore(storeOptions); + const store = await redisStore(storeOptions); - store.client - .on('error', (err) => { - console.log('Redis Cache Client Error: ', err); - }) - .on('connect', () => { - console.log('Redis Cache Client Connected'); - }) - .on('ready', () => { - console.log('Redis Cache Client Ready'); - }) - .on('reconnecting', () => { - console.log('Redis Cache Client Reconnecting'); - }) - .on('end', () => { - console.log('Redis Cache Client End'); - }); + store.client + .on('error', (err) => { + console.log('Redis Cache Client Error: ', err); + }) + .on('connect', () => { + console.log('Redis Cache Client Connected'); + }) + .on('ready', () => { + console.log('Redis Cache Client Ready'); + }) + .on('reconnecting', () => { + console.log('Redis Cache Client Reconnecting'); + }) + .on('end', () => { + console.log('Redis Cache Client End'); + }); - // ping Redis - const res = await store.client.ping(); - console.log('Redis Cache Client Cache Ping: ', res); + // ping Redis + const res = await store.client.ping(); + console.log('Redis Cache Client Cache Ping: ', res); - return { - store: () => store - }; - } - }) - ] + return { + store: () => store + }; + } + }) + ] : [CacheModule.register({ isGlobal: true })]), ServeStaticModule.forRootAsync({ useFactory: async (configService: ConfigService): Promise => { @@ -326,18 +322,18 @@ if (environment.THROTTLE_ENABLED) { }), ...(environment.THROTTLE_ENABLED ? [ - ThrottlerModule.forRootAsync({ - inject: [ConfigService], - useFactory: () => { - return [ - { - ttl: environment.THROTTLE_TTL, - limit: environment.THROTTLE_LIMIT - } - ]; - } - }) - ] + ThrottlerModule.forRootAsync({ + inject: [ConfigService], + useFactory: () => { + return [ + { + ttl: environment.THROTTLE_TTL, + limit: environment.THROTTLE_LIMIT + } + ]; + } + }) + ] : []), HealthModule, CoreModule, @@ -363,9 +359,6 @@ if (environment.THROTTLE_ENABLED) { ExportModule, ImportModule, EmployeeSettingModule, - EmployeeJobPresetModule, - EmployeeJobPostModule, - EmployeeProposalTemplateModule, EmployeeStatisticsModule, EmployeeAppointmentModule, AppointmentEmployeesModule, @@ -389,7 +382,6 @@ if (environment.THROTTLE_ENABLED) { OrganizationDocumentModule, RequestApprovalEmployeeModule, RequestApprovalTeamModule, - ProposalModule, EmailHistoryModule, EmailTemplateModule, CountryModule, @@ -471,11 +463,11 @@ if (environment.THROTTLE_ENABLED) { AppService, ...(environment.THROTTLE_ENABLED ? [ - { - provide: APP_GUARD, - useClass: ThrottlerBehindProxyGuard - } - ] + { + provide: APP_GUARD, + useClass: ThrottlerBehindProxyGuard + } + ] : []), { provide: APP_INTERCEPTOR, diff --git a/packages/core/src/auth/auth.service.ts b/packages/core/src/auth/auth.service.ts index cab860ee154..a78d37709dc 100644 --- a/packages/core/src/auth/auth.service.ts +++ b/packages/core/src/auth/auth.service.ts @@ -1040,7 +1040,7 @@ export class AuthService extends SocialAuthService { */ private async createWorkspace(user: IUser, code: string, includeTeams: boolean): Promise { const tenantId = user.tenant ? user.tenantId : null; - const employeeId = user.employee ? user.employee?.id : null; + const employeeId = await this.employeeService.findEmployeeIdByUserId(user.id); const workspace: IWorkspaceResponse = { user: this.createUserObject(user), @@ -1076,13 +1076,11 @@ export class AuthService extends SocialAuthService { email: user.email || null, // Sets email to null if it's undefined name: user.name || null, // Sets name to null if it's undefined imageUrl: user.imageUrl || null, // Sets imageUrl to null if it's undefined - tenant: user.tenant - ? new Tenant({ - id: user.tenant.id, // Assuming tenantId is a direct property of tenant - name: user.tenant.name || '', // Defaulting to an empty string if name is undefined - logo: user.tenant.logo || '' // Defaulting to an empty string if logo is undefined - }) - : null // Sets tenant to null if user.tenant is undefined + tenant: user.tenant ? new Tenant({ + id: user.tenant.id, // Assuming tenantId is a direct property of tenant + name: user.tenant.name || '', // Defaulting to an empty string if name is undefined + logo: user.tenant.logo || '' // Defaulting to an empty string if logo is undefined + }) : null // Sets tenant to null if user.tenant is undefined }); } } diff --git a/packages/core/src/bootstrap/index.ts b/packages/core/src/bootstrap/index.ts index ddd117befab..9e300333861 100644 --- a/packages/core/src/bootstrap/index.ts +++ b/packages/core/src/bootstrap/index.ts @@ -47,10 +47,12 @@ import { getConfig, setConfig, environment as env } from '@gauzy/config'; import { getEntitiesFromPlugins, getPluginConfigurations, getSubscribersFromPlugins } from '@gauzy/plugin'; import { coreEntities } from '../core/entities'; import { coreSubscribers } from '../core/entities/subscribers'; +import { AuthGuard } from '../shared/guards'; +import { SharedModule } from '../shared/shared.module'; +import { registerCustomEntityFields } from '../core/entities/custom-entity-fields/register-custom-entity-fields'; import { AppService } from '../app.service'; import { AppModule } from '../app.module'; -import { AuthGuard } from '../shared/guards'; -import { SharedModule } from './../shared/shared.module'; + export async function bootstrap(pluginConfig?: Partial): Promise { console.time('Application Bootstrap Time'); @@ -329,16 +331,18 @@ export async function registerPluginConfig(pluginConfig: Partial { +async function bootstrapPluginConfigurations(config: ApplicationPluginConfig): Promise { const pluginConfigurations = getPluginConfigurations(config.plugins); for await (const pluginConfigurationFn of pluginConfigurations) { diff --git a/packages/core/src/core/crud/crud.factory.ts b/packages/core/src/core/crud/crud.factory.ts index c7fea42d10a..475e253c2a8 100644 --- a/packages/core/src/core/crud/crud.factory.ts +++ b/packages/core/src/core/crud/crud.factory.ts @@ -1,9 +1,9 @@ -import { Body, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query, Type, UsePipes } from '@nestjs/common'; +import { Body, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query, UsePipes } from '@nestjs/common'; import { ApiOperation, ApiResponse } from '@nestjs/swagger'; import { DeepPartial, DeleteResult, FindOptionsWhere, UpdateResult } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { IPagination } from '@gauzy/contracts'; -import { ConstructorType } from '@gauzy/common'; +import { Type } from '@gauzy/common'; import { BaseEntity } from './../../core/entities/base.entity'; import { AbstractValidationPipe, UUIDValidationPipe } from './../../shared/pipes'; import { ICrudController } from './icrud.controller'; @@ -22,7 +22,7 @@ export function CrudFactory, updateDTO?: Type, countQueryDTO?: Type -): ConstructorType> { +): Type> { class BaseCrudController implements ICrudController { constructor(public readonly crudService: ICrudService) { } diff --git a/packages/core/src/core/crud/crud.service.ts b/packages/core/src/core/crud/crud.service.ts index 3897a93c6d0..565bbac5423 100644 --- a/packages/core/src/core/crud/crud.service.ts +++ b/packages/core/src/core/crud/crud.service.ts @@ -442,13 +442,13 @@ export abstract class CrudService implements ICrudService< */ public async create( partialEntity: IPartialEntity, - createOptions: CreateOptions = { + createOptions: CreateOptions = { /** This option disables the strict typing which requires all mandatory properties to have value, it has no effect on runtime */ partial: true, /** Creates a managed entity instance instead, bypassing the constructor call */ managed: true }, - assignOptions: AssignOptions = { + assignOptions: AssignOptions = { updateNestedEntities: false, onlyOwnProperties: true } diff --git a/packages/core/src/core/decorators/entity/embedded-column.decorator.ts b/packages/core/src/core/decorators/entity/embedded-column.decorator.ts new file mode 100644 index 00000000000..3bdf4106725 --- /dev/null +++ b/packages/core/src/core/decorators/entity/embedded-column.decorator.ts @@ -0,0 +1,65 @@ +import { EmbeddedOptions as MikroOrmColumnEmbeddedOptions } from '@mikro-orm/core'; +import { Column, ObjectType } from 'typeorm'; +import { ColumnEmbeddedOptions as TypeOrmColumnEmbeddedOptions } from 'typeorm/decorator/options/ColumnEmbeddedOptions'; + +// Represents combined options for mapping an embeddable column in both TypeORM and Mikro ORM. +type CombineColumnEmbeddedOptions = TypeOrmColumnEmbeddedOptions & MikroOrmColumnEmbeddedOptions; +// Represents a function that can be used to obtain the type of an entity +type TargetEntity = (type?: any) => ObjectType; + +/** + * Represents the options for mapping an embeddable column in Mikro ORM. + */ +interface EmbeddableColumnMikroORMOptions { + /** + * Represents the type, function, or target entity for the Mikro ORM column. + */ + typeFunctionOrTarget: TargetEntity; + + /** + * (Optional) Additional options for the Mikro ORM and Type ORM column. + */ + options?: CombineColumnEmbeddedOptions; +} + +/** + * A decorator factory for mapping an embeddable column in Mikro ORM. + * @param typeFunctionOrTarget The type, function, or target entity for the MikroORM & TypeORM Embedded column. + * @param options Additional options for the MikroORM & TypeORM column. + * @returns A property decorator. + */ +export function EmbeddedColumn( + typeFunctionOrTarget?: (type?: any) => ObjectType, + options?: CombineColumnEmbeddedOptions +): PropertyDecorator { + // If options are not provided, initialize an empty object + if (!options) options = {} as CombineColumnEmbeddedOptions; + + // Return a property decorator function + return (target: any, propertyKey: string) => { + // Apply the @Embedded decorator with mapped Mikro ORM options + // Embedded(parseEmbeddableColumnMikroORMOptions({ typeFunctionOrTarget, options }))(target, propertyKey); + + // Apply the @Column decorator from TypeORM + Column(typeFunctionOrTarget as TargetEntity, options)(target, propertyKey); + }; +} + +/** + * Maps EmbeddableColumnMikroORMOptions to MikroOrmColumnEmbeddedOptions. + * + * @param param0 The EmbeddableColumnMikroORMOptions to map. + * @returns The mapped MikroOrmColumnEmbeddedOptions. + */ +export function parseEmbeddableColumnMikroORMOptions({ typeFunctionOrTarget, options }: EmbeddableColumnMikroORMOptions): MikroOrmColumnEmbeddedOptions { + // Create a partial MikroOrmColumnEmbeddedOptions object to store the mapped options. + const mikroOrmColumnEmbeddedOptions: Partial = { + // Map the typeFunctionOrTarget to the 'entity' property. + entity: typeFunctionOrTarget as string | (() => T | T[]), + // Assign the 'prefix' option from EmbeddableColumnMikroORMOptions, if provided. + prefix: options?.prefix + }; + + // Return the mapped MikroOrmColumnEmbeddedOptions. + return mikroOrmColumnEmbeddedOptions as MikroOrmColumnEmbeddedOptions; +} diff --git a/packages/core/src/core/decorators/entity/entity-options.types.ts b/packages/core/src/core/decorators/entity/entity-options.types.ts index de0d0e5f144..7937989d881 100644 --- a/packages/core/src/core/decorators/entity/entity-options.types.ts +++ b/packages/core/src/core/decorators/entity/entity-options.types.ts @@ -1,6 +1,6 @@ import { EntityOptions as TypeEntityOptions } from 'typeorm'; import { EntityOptions as MikroEntityOptions } from '@mikro-orm/core'; -import { ConstructorType } from '@gauzy/common'; +import { Type } from '@gauzy/common'; /** * Options for defining MikroORM entities. @@ -11,7 +11,7 @@ export type MikroOrmEntityOptions = MikroEntityOptions & { /** * Optional function returning the repository constructor. */ - mikroOrmRepository?: () => ConstructorType; + mikroOrmRepository?: () => Type; }; /** @@ -21,5 +21,5 @@ export type TypeOrmEntityOptions = TypeEntityOptions & { /** * Optional function returning the repository constructor. */ - typeOrmRepository?: () => ConstructorType; + typeOrmRepository?: () => Type; }; diff --git a/packages/core/src/core/decorators/entity/index.ts b/packages/core/src/core/decorators/entity/index.ts index f3e163f932f..6ab80197f5c 100644 --- a/packages/core/src/core/decorators/entity/index.ts +++ b/packages/core/src/core/decorators/entity/index.ts @@ -3,3 +3,4 @@ export * from './column.decorator'; export * from './column-index.decorator'; export * from './relations'; export * from './virtual-column.decorator'; +export * from './embedded-column.decorator'; diff --git a/packages/core/src/core/dto/index.ts b/packages/core/src/core/dto/index.ts index 4d56416f4fd..7d1f8c6ee94 100644 --- a/packages/core/src/core/dto/index.ts +++ b/packages/core/src/core/dto/index.ts @@ -1,3 +1,3 @@ -export { TenantBaseDTO } from './tenant-base.dto'; -export { TenantOrganizationBaseDTO } from './tenant-organization-base.dto'; -export { TranslatableBaseDTO } from './translate-base-dto'; \ No newline at end of file +export * from './tenant-base.dto'; +export * from './tenant-organization-base.dto'; +export * from './translate-base-dto'; diff --git a/packages/core/src/core/entities/base.entity.ts b/packages/core/src/core/entities/base.entity.ts index e7e2f4b7e83..36f710e2eb5 100644 --- a/packages/core/src/core/entities/base.entity.ts +++ b/packages/core/src/core/entities/base.entity.ts @@ -43,7 +43,7 @@ export abstract class BaseEntity extends Model implements IBaseEntityModel { // Automatically set the property value when entity gets created, executed during flush operation. onCreate: () => new Date() }) - createdAt?: Date = new Date(); + createdAt?: Date; // Date when the record was last updated @ApiPropertyOptional({ @@ -58,7 +58,7 @@ export abstract class BaseEntity extends Model implements IBaseEntityModel { // Automatically update the property value every time entity gets updated, executed during flush operation. onUpdate: () => new Date() }) - updatedAt?: Date = new Date();; + updatedAt?: Date; // Soft Delete @ApiPropertyOptional({ diff --git a/packages/core/src/core/entities/custom-entity-fields/custom-entity-fields.ts b/packages/core/src/core/entities/custom-entity-fields/custom-entity-fields.ts new file mode 100644 index 00000000000..fa70efa2ac7 --- /dev/null +++ b/packages/core/src/core/entities/custom-entity-fields/custom-entity-fields.ts @@ -0,0 +1,7 @@ +import { Embeddable } from '@mikro-orm/knex'; + +@Embeddable() +export class CustomEmployeeFields { } + +@Embeddable() +export class CustomTagFields { } diff --git a/packages/core/src/core/entities/custom-entity-fields/custom-field-types.ts b/packages/core/src/core/entities/custom-entity-fields/custom-field-types.ts new file mode 100644 index 00000000000..ba963124810 --- /dev/null +++ b/packages/core/src/core/entities/custom-entity-fields/custom-field-types.ts @@ -0,0 +1,9 @@ +import { CustomFieldsObject } from '@gauzy/common'; + +/** + * This interface should be implemented by any entity which can be extended + * with custom fields. + */ +export interface HasCustomFields { + customFields?: CustomFieldsObject; +} diff --git a/packages/core/src/core/entities/custom-entity-fields/index.ts b/packages/core/src/core/entities/custom-entity-fields/index.ts new file mode 100644 index 00000000000..724ff6e0a61 --- /dev/null +++ b/packages/core/src/core/entities/custom-entity-fields/index.ts @@ -0,0 +1,3 @@ +export * from './custom-entity-fields'; +export * from './custom-field-types'; +export * from './register-custom-entity-fields'; diff --git a/packages/core/src/core/entities/custom-entity-fields/register-custom-entity-fields.ts b/packages/core/src/core/entities/custom-entity-fields/register-custom-entity-fields.ts new file mode 100644 index 00000000000..951ba61c37b --- /dev/null +++ b/packages/core/src/core/entities/custom-entity-fields/register-custom-entity-fields.ts @@ -0,0 +1,82 @@ +import { JoinColumn } from 'typeorm'; +import { ApplicationPluginConfig, CustomEmbeddedFields, RelationCustomEmbeddedFieldConfig } from '@gauzy/common'; +import { MultiORMColumn, MultiORMManyToMany, MultiORMManyToOne } from '../../../core/decorators'; +import { ColumnDataType, ColumnOptions } from '../../../core/decorators/entity/column-options.types'; +import { CustomEmployeeFields, CustomTagFields } from './custom-entity-fields'; + +export const __FIX_RELATIONAL_CUSTOM_FIELDS__ = '__fix_relational_custom_fields__'; + +/** + * Registers a custom column or relation for the entity based on the provided custom field configuration. + * + * @param customField The custom field configuration. + * @param name The name of the custom column or relation. + * @param instance The instance of the entity class. + */ +export const registerColumn = async ( + customField: RelationCustomEmbeddedFieldConfig, + name: string, + instance: any +): Promise => { + if (customField.type === 'relation') { + if (customField.relationType === 'many-to-many') { + // Use MultiORMManyToMany decorator to register Many-to-Many relation + MultiORMManyToMany(() => customField.entity, customField.inverseSide)(instance, name); + } + if (customField.relationType === 'many-to-one') { + // Use MultiORMManyToOne decorator to register Many-to-One relation + MultiORMManyToOne(() => customField.entity, customField.inverseSide)(instance, name); + JoinColumn()(instance, name); + } + } else { + const { nullable, unique } = customField; + const options: ColumnDataType | ColumnOptions = { + name, + nullable: nullable === false ? false : true, + unique: unique ?? false, + }; + MultiORMColumn(options)(instance, name); + // Logic to handle custom column registration + } +}; + +/** + * Registers custom fields for a specific entity in the provided application configuration. + * + * @param config The application configuration. + * @param entityName The name of the entity for which custom fields are registered. + * @param ctor The constructor function for the custom fields. + */ +function registerCustomFieldsForEntity( + config: ApplicationPluginConfig, + entityName: keyof CustomEmbeddedFields, + ctor: { new(): T } +): void { + const customFields = config.customFields?.[entityName] ?? []; + const instance = new ctor(); + + for (const customField of customFields) { + const { propertyPath } = customField; + registerColumn(customField, propertyPath, instance); + } + + /** + * If there are only relations are defined for an Entity for customFields, then TypeORM not saving realtions for entity ("Cannot set properties of undefined ()"). + * So we have to add a "fake" column to the customFields embedded type to prevent this error from occurring. + */ + if (customFields.length > 0) { + MultiORMColumn({ + type: 'boolean', + nullable: true + })(instance, __FIX_RELATIONAL_CUSTOM_FIELDS__); + } +} + +/** + * Registers custom entity fields for the provided application configuration. + * @param config The application configuration. + */ +export async function registerCustomEntityFields(config: ApplicationPluginConfig) { + registerCustomFieldsForEntity(config, 'Tag', CustomTagFields); + registerCustomFieldsForEntity(config, 'Employee', CustomEmployeeFields); +} diff --git a/packages/core/src/core/entities/index.ts b/packages/core/src/core/entities/index.ts index 212c2a456d1..d509311d95d 100644 --- a/packages/core/src/core/entities/index.ts +++ b/packages/core/src/core/entities/index.ts @@ -29,10 +29,8 @@ import { EmployeeAward, EmployeeLevel, EmployeePhone, - EmployeeProposalTemplate, EmployeeRecurringExpense, EmployeeSetting, - EmployeeUpworkJobsSearchCriterion, Equipment, EquipmentSharing, EquipmentSharingPolicy, @@ -64,10 +62,6 @@ import { InvoiceEstimateHistory, InvoiceItem, IssueType, - JobPreset, - JobPresetUpworkJobSearchCriterion, - JobSearchCategory, - JobSearchOccupation, KeyResult, KeyResultTemplate, KeyResultUpdate, @@ -108,7 +102,6 @@ import { ProductVariant, ProductVariantPrice, ProductVariantSetting, - Proposal, Report, ReportCategory, ReportOrganization, @@ -174,10 +167,8 @@ export const coreEntities = [ EmployeeAward, EmployeeLevel, EmployeePhone, - EmployeeProposalTemplate, EmployeeRecurringExpense, EmployeeSetting, - EmployeeUpworkJobsSearchCriterion, Equipment, EquipmentSharing, EquipmentSharingPolicy, @@ -209,10 +200,6 @@ export const coreEntities = [ InvoiceEstimateHistory, InvoiceItem, IssueType, - JobPreset, - JobPresetUpworkJobSearchCriterion, - JobSearchCategory, - JobSearchOccupation, KeyResult, KeyResultTemplate, KeyResultUpdate, @@ -253,7 +240,6 @@ export const coreEntities = [ ProductVariant, ProductVariantPrice, ProductVariantSetting, - Proposal, Report, ReportCategory, ReportOrganization, diff --git a/packages/core/src/core/entities/internal.ts b/packages/core/src/core/entities/internal.ts index 21f3d986362..32223a72b49 100644 --- a/packages/core/src/core/entities/internal.ts +++ b/packages/core/src/core/entities/internal.ts @@ -31,16 +31,8 @@ export * from '../../email-reset/email-reset.entity'; export * from '../../email-template/email-template.entity'; export * from '../../employee-appointment/employee-appointment.entity'; export * from '../../employee-award/employee-award.entity'; -export * from '../../employee-job-preset/employee-upwork-jobs-search-criterion.entity'; -export * from '../../employee-job-preset/job-preset-upwork-job-search-criterion.entity'; -export * from '../../employee-job-preset/job-preset.entity'; -export * from '../../employee-job-preset/job-search-category/job-search-category.entity'; -export * from '../../employee-job-preset/job-search-occupation/job-search-occupation.entity'; -export * from '../../employee-job/employee-job.entity'; -export * from '../../employee-job/jobPost.entity'; export * from '../../employee-level/employee-level.entity'; export * from '../../employee-phone/employee-phone.entity'; -export * from '../../employee-proposal-template/employee-proposal-template.entity'; export * from '../../employee-recurring-expense/employee-recurring-expense.entity'; export * from '../../employee-setting/employee-setting.entity'; export * from '../../employee/employee.entity'; @@ -114,7 +106,6 @@ export * from '../../product-variant-price/product-variant-price.entity'; export * from '../../product-variant/product-variant.entity'; export * from '../../product/product-translation.entity'; export * from '../../product/product.entity'; -export * from '../../proposal/proposal.entity'; export * from '../../reports/report-category.entity'; export * from '../../reports/report-organization.entity'; export * from '../../reports/report.entity'; diff --git a/packages/core/src/core/index.ts b/packages/core/src/core/index.ts index 9fef4528ecf..461a0c71d34 100644 --- a/packages/core/src/core/index.ts +++ b/packages/core/src/core/index.ts @@ -7,5 +7,6 @@ export * from './repository'; export * from './entities/internal'; export * from './entities/subscribers'; export * from './decorators'; +export * from './dto'; export * from './orm-type'; export * from './plugin-common.module'; diff --git a/packages/core/src/core/seeds/seed-data.service.ts b/packages/core/src/core/seeds/seed-data.service.ts index 42812a49355..a5323fc8e4c 100644 --- a/packages/core/src/core/seeds/seed-data.service.ts +++ b/packages/core/src/core/seeds/seed-data.service.ts @@ -171,10 +171,6 @@ import { createDefaultEquipmentSharing, createRandomEquipmentSharing, } from '../../equipment-sharing/equipment-sharing.seed'; -import { - createDefaultProposals, - createRandomProposals, -} from '../../proposal/proposal.seed'; import { createDefaultInvoiceItem, createRandomInvoiceItem, @@ -289,8 +285,6 @@ import { createDefaultKeyResultTemplates } from '../../keyresult-template/keyres import { createDefaultEmployeeAwards } from '../../employee-award/employee-award.seed'; import { createDefaultGoalKpiTemplate } from '../../goal-kpi-template/goal-kpi-template.seed'; import { randomSeedConfig } from './random-seed-config'; -import { createDefaultJobSearchCategories } from '../../employee-job-preset/job-search-category/job-search-category.seed'; -import { createDefaultJobSearchOccupations } from '../../employee-job-preset/job-search-occupation/job-search-occupation.seed'; import { createDefaultReport, createRandomTenantOrganizationsReport, @@ -321,6 +315,9 @@ export enum SeederTypeEnum { @Injectable() export class SeedDataService { + + dataSource: DataSource; + log = console.log; defaultOrganization: IOrganization; tenant: ITenant; @@ -333,7 +330,10 @@ export class SeedDataService { defaultCandidateUsers: IUser[] = []; defaultEmployees: IEmployee[] = []; - dataSource: DataSource; + /** */ + randomTenants: ITenant[]; + randomTenantOrganizationsMap: Map; + randomOrganizationEmployeesMap: Map; constructor( private readonly moduleRef: ModuleRef, @@ -377,9 +377,6 @@ export class SeedDataService { // Seed data with mock / fake data for random tenants await this.seedRandomData(); - // Seed jobs related data - await this.seedJobsData(); - // Disconnect to database await this.closeConnection(); @@ -493,9 +490,6 @@ export class SeedDataService { // Seed random data await this.seedRandomData(); - // Seed jobs related data - await this.seedJobsData(); - // Disconnect to database await this.closeConnection(); @@ -541,53 +535,8 @@ export class SeedDataService { * Seed Default Job Data */ public async runJobsSeed() { - this.seedType = SeederTypeEnum.ALL; try { - // Seed jobs related data - await this.seedJobsData(); - - console.log('Database Jobs Seed Completed'); - } catch (error) { - this.handleError(error); - } - } - - /** - * Populate database with jobs related data - */ - private async seedJobsData() { - try { - this.log( - chalk.green( - `🌱 SEEDING ${env.production ? 'PRODUCTION' : '' - } JOBS DATABASE...` - ) - ); - - await this.tryExecute( - 'Default Job Search Categories', - createDefaultJobSearchCategories( - this.dataSource, - this.tenant, - this.defaultOrganization - ) - ); - - await this.tryExecute( - 'Default Job Search Occupations', - createDefaultJobSearchOccupations( - this.dataSource, - this.tenant, - this.defaultOrganization - ) - ); - - this.log( - chalk.green( - `✅ SEEDED ${env.production ? 'PRODUCTION' : '' - } JOBS DATABASE` - ) - ); + this.seedType = SeederTypeEnum.ALL; } catch (error) { this.handleError(error); } @@ -1156,17 +1105,6 @@ export class SeedDataService { ) ); - await this.tryExecute( - 'Default Proposals', - createDefaultProposals( - this.dataSource, - this.tenant, - this.defaultEmployees, - this.organizations, - randomSeedConfig.proposalsSharingPerOrganizations || 30 - ) - ); - await this.tryExecute( 'Default Organization Languages', createDefaultOrganizationLanguage( @@ -1382,50 +1320,50 @@ export class SeedDataService { await this.tryExecute('Random Tags', createTags(this.dataSource)); // Platform level data which only need database connection - const tenants = await createRandomTenants( + this.randomTenants = await createRandomTenants( this.dataSource, randomSeedConfig.tenants || 1 ); await this.tryExecute( 'Random Tenant Settings', - createDefaultTenantSetting(this.dataSource, tenants) + createDefaultTenantSetting(this.dataSource, this.randomTenants) ); await this.tryExecute( 'Random Feature Reports', - createRandomTenantOrganizationsReport(this.dataSource, tenants) + createRandomTenantOrganizationsReport(this.dataSource, this.randomTenants) ); await this.tryExecute( 'Random Feature Toggle', - createRandomFeatureToggle(this.dataSource, tenants) + createRandomFeatureToggle(this.dataSource, this.randomTenants) ); // Independent roles and role permissions for each tenant - const roles: IRole[] = await createRoles(this.dataSource, tenants); + const roles: IRole[] = await createRoles(this.dataSource, this.randomTenants); await this.tryExecute( 'Random Tenant Role Permissions', - createRolePermissions(this.dataSource, roles, tenants) + createRolePermissions(this.dataSource, roles, this.randomTenants) ); // Tenant level inserts which only need connection, tenant, role - const tenantOrganizationsMap = await createRandomOrganizations( + this.randomTenantOrganizationsMap = await createRandomOrganizations( this.dataSource, - tenants, + this.randomTenants, randomSeedConfig.organizationsPerTenant || 1 ); const tenantSuperAdminsMap = await createRandomSuperAdminUsers( this.dataSource, - tenants, + this.randomTenants, 1 ); const tenantUsersMap = await createRandomUsers( this.dataSource, - tenants, + this.randomTenants, randomSeedConfig.adminPerOrganization || 1, randomSeedConfig.organizationsPerTenant || 1, randomSeedConfig.employeesPerOrganization || 1, @@ -1438,18 +1376,18 @@ export class SeedDataService { // Organization level inserts which need connection, tenant, organizations, users const organizationUsersMap = await createRandomUsersOrganizations( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, tenantSuperAdminsMap, tenantUsersMap, randomSeedConfig.employeesPerOrganization || 1, randomSeedConfig.adminPerOrganization || 1 ); - const organizationEmployeesMap = await createRandomEmployees( + this.randomOrganizationEmployeesMap = await createRandomEmployees( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, organizationUsersMap ); @@ -1457,8 +1395,8 @@ export class SeedDataService { 'Random Organization Tags', createRandomOrganizationTags( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1466,8 +1404,8 @@ export class SeedDataService { 'Random Organization Documents', createRandomOrganizationDocuments( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1475,8 +1413,8 @@ export class SeedDataService { 'Random Product Categories', createRandomProductCategories( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1484,8 +1422,8 @@ export class SeedDataService { 'Random Product Types', createRandomProductType( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1493,8 +1431,8 @@ export class SeedDataService { 'Random Products', createRandomProduct( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1502,8 +1440,8 @@ export class SeedDataService { 'Random Product Option Groups', createRandomProductOptionGroups( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, randomSeedConfig.numberOfOptionGroupPerProduct || 5 ) ); @@ -1512,8 +1450,8 @@ export class SeedDataService { 'Random Product Options', createRandomProductOption( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, randomSeedConfig.numberOfOptionPerProduct || 5 ) ); @@ -1522,8 +1460,8 @@ export class SeedDataService { 'Random Product Variants', createRandomProductVariant( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, randomSeedConfig.numberOfVariantPerProduct || 5 ) ); @@ -1532,8 +1470,8 @@ export class SeedDataService { 'Random Product Variant Prices', createRandomProductVariantPrice( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1541,8 +1479,8 @@ export class SeedDataService { 'Random Warehouses', createRandomWarehouses( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1550,8 +1488,8 @@ export class SeedDataService { 'Random Merchants', createRandomMerchants( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1559,8 +1497,8 @@ export class SeedDataService { 'Random Product Variant Settings', createRandomProductVariantSettings( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1568,9 +1506,9 @@ export class SeedDataService { 'Random Incomes', createRandomIncomes( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -1578,10 +1516,10 @@ export class SeedDataService { 'Random Organization Teams', createRandomTeam( this.dataSource, - tenants, + this.randomTenants, roles, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -1589,9 +1527,9 @@ export class SeedDataService { 'Random Goals', createRandomGoal( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -1599,10 +1537,10 @@ export class SeedDataService { 'Random Key Results', createRandomKeyResult( this.dataSource, - tenants, + this.randomTenants, randomGoals, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -1610,8 +1548,8 @@ export class SeedDataService { 'Random Candidates', createRandomCandidates( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, tenantUsersMap, randomSeedConfig.candidatesPerOrganization || 1 ) @@ -1621,7 +1559,7 @@ export class SeedDataService { 'Random Candidate Sources', createRandomCandidateSources( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); @@ -1630,7 +1568,7 @@ export class SeedDataService { 'Random Candidate Documents', createRandomCandidateDocuments( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); @@ -1639,7 +1577,7 @@ export class SeedDataService { 'Random Candidate Experiences', createRandomCandidateExperience( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); @@ -1648,7 +1586,7 @@ export class SeedDataService { 'Random Candidate Skills', createRandomCandidateSkills( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); @@ -1657,8 +1595,8 @@ export class SeedDataService { 'Random Organization Vendors', createRandomOrganizationVendors( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1666,8 +1604,8 @@ export class SeedDataService { 'Random Time Off Policies', createRandomTimeOffPolicies( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1675,8 +1613,8 @@ export class SeedDataService { 'Random Expense Categories', createRandomExpenseCategories( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1684,9 +1622,9 @@ export class SeedDataService { 'Random Expenses', createRandomExpenses( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap, + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap, organizationVendorsMap, categoriesMap ) @@ -1696,7 +1634,7 @@ export class SeedDataService { 'Random Equipments', createRandomEquipments( this.dataSource, - tenants, + this.randomTenants, randomSeedConfig.equipmentPerTenant || 20 ) ); @@ -1705,9 +1643,9 @@ export class SeedDataService { 'Random Equipment Sharing', createRandomEquipmentSharing( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap, + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap, randomSeedConfig.equipmentSharingPerTenant || 20 ) ); @@ -1716,8 +1654,8 @@ export class SeedDataService { 'Random Employment Types', seedRandomEmploymentTypes( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1725,8 +1663,8 @@ export class SeedDataService { 'Random Organization Departments', seedRandomOrganizationDepartments( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1734,8 +1672,8 @@ export class SeedDataService { 'Random Employee Invites', createRandomEmployeeInviteSent( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, tenantSuperAdminsMap, randomSeedConfig.invitePerOrganization || 20 ) @@ -1745,8 +1683,8 @@ export class SeedDataService { 'Random Organization Positions', seedRandomOrganizationPosition( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1754,8 +1692,8 @@ export class SeedDataService { 'Random Approval Policies', createRandomApprovalPolicyForOrg( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1763,8 +1701,8 @@ export class SeedDataService { 'Random Equipment Sharing Policies', createRandomEquipmentSharingPolicy( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1772,9 +1710,9 @@ export class SeedDataService { 'Random Request Approvals', createRandomRequestApproval( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap, + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap, randomSeedConfig.requestApprovalPerOrganization || 20 ) ); @@ -1783,8 +1721,8 @@ export class SeedDataService { 'Random Organization Projects', createRandomOrganizationProjects( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, tags, randomSeedConfig.projectsPerOrganization || 10 ) @@ -1794,44 +1732,33 @@ export class SeedDataService { 'Random Employee Time Off', createRandomEmployeeTimeOff( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap, + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap, randomSeedConfig.employeeTimeOffPerOrganization || 20 ) ); - await this.tryExecute( - 'Random Proposals', - createRandomProposals( - this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap, - randomSeedConfig.proposalsSharingPerOrganizations || 30 - ) - ); - await this.tryExecute( 'Random Email Sent', createRandomEmailSent( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, randomSeedConfig.emailsPerOrganization || 20 ) ); await this.tryExecute( 'Random Tasks', - createRandomTask(this.dataSource, tenants) + createRandomTask(this.dataSource, this.randomTenants) ); await this.tryExecute( 'Random Organization Contacts', createRandomOrganizationContact( this.dataSource, - tenants, + this.randomTenants, randomSeedConfig.noOfContactsPerOrganization ) ); @@ -1840,8 +1767,8 @@ export class SeedDataService { 'Random Invoices', createRandomInvoice( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, randomSeedConfig.numberOfInvoicePerOrganization || 50 ) ); @@ -1850,8 +1777,8 @@ export class SeedDataService { 'Random Invoice Items', createRandomInvoiceItem( this.dataSource, - tenants, - tenantOrganizationsMap, + this.randomTenants, + this.randomTenantOrganizationsMap, randomSeedConfig.numberOfInvoiceItemPerInvoice || 5 ) ); @@ -1860,9 +1787,9 @@ export class SeedDataService { 'Random Availability Slots', createRandomAvailabilitySlots( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap, + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap, randomSeedConfig.availabilitySlotsPerOrganization || 20 ) ); @@ -1871,9 +1798,9 @@ export class SeedDataService { 'Random Payments', createRandomPayment( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -1881,7 +1808,7 @@ export class SeedDataService { 'Random Candidate Educations', createRandomCandidateEducations( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); @@ -1890,7 +1817,7 @@ export class SeedDataService { 'Random Candidate Interviews', createRandomCandidateInterview( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); @@ -1899,7 +1826,7 @@ export class SeedDataService { 'Random Candidate Technologies', createRandomCandidateTechnologies( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); @@ -1908,23 +1835,27 @@ export class SeedDataService { 'Random Candidate Personal Qualities', createRandomCandidatePersonalQualities( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); await this.tryExecute( 'Random Awards', - createRandomAwards(this.dataSource, tenants, tenantOrganizationsMap) + createRandomAwards( + this.dataSource, + this.randomTenants, + this.randomTenantOrganizationsMap + ) ); await this.tryExecute( 'Random Candidate Interviewers', createRandomCandidateInterviewers( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap, + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap, tenantCandidatesMap ) ); @@ -1933,7 +1864,7 @@ export class SeedDataService { 'Random Candidate Feedbacks', createRandomCandidateFeedbacks( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); @@ -1942,9 +1873,9 @@ export class SeedDataService { 'Random Employee Recurring Expenses', createRandomEmployeeRecurringExpense( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -1952,9 +1883,9 @@ export class SeedDataService { 'Random Employee Settings', createRandomEmployeeSetting( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -1962,8 +1893,8 @@ export class SeedDataService { 'Random Organization Languages', createRandomOrganizationLanguage( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1971,8 +1902,8 @@ export class SeedDataService { 'Random Organization Recurring Expenses', createRandomOrganizationRecurringExpense( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1980,8 +1911,8 @@ export class SeedDataService { 'Random Organization Sprints', createRandomOrganizationSprint( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -1989,9 +1920,9 @@ export class SeedDataService { 'Random Organization Team Employees', createRandomOrganizationTeamEmployee( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -1999,9 +1930,9 @@ export class SeedDataService { 'Random Appointment Employees', createRandomAppointmentEmployees( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -2009,9 +1940,9 @@ export class SeedDataService { 'Random Employee Appointments', createRandomEmployeeAppointment( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -2019,8 +1950,8 @@ export class SeedDataService { 'Random Pipelines', createRandomPipeline( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -2028,8 +1959,8 @@ export class SeedDataService { 'Random Pipeline Stages', createRandomPipelineStage( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -2037,44 +1968,44 @@ export class SeedDataService { 'Random Deals', createRandomDeal( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); await this.tryExecute( 'Random Integrations', - createRandomIntegrationTenant(this.dataSource, tenants) + createRandomIntegrationTenant(this.dataSource, this.randomTenants) ); await this.tryExecute( 'Random Integration Settings', - createRandomIntegrationSetting(this.dataSource, tenants) + createRandomIntegrationSetting(this.dataSource, this.randomTenants) ); await this.tryExecute( 'Random Integration Map', - createRandomIntegrationMap(this.dataSource, tenants) + createRandomIntegrationMap(this.dataSource, this.randomTenants) ); await this.tryExecute( 'Random Integration Entity Settings', - createRandomIntegrationEntitySetting(this.dataSource, tenants) + createRandomIntegrationEntitySetting(this.dataSource, this.randomTenants) ); await this.tryExecute( 'Random Integration Entity Settings Tied Entity', - createRandomIntegrationEntitySettingTied(this.dataSource, tenants) + createRandomIntegrationEntitySettingTied(this.dataSource, this.randomTenants) ); await this.tryExecute( 'Random Request Approval Employee', createRandomRequestApprovalEmployee( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -2082,8 +2013,8 @@ export class SeedDataService { 'Random Request Approval Team', createRandomRequestApprovalTeam( this.dataSource, - tenants, - tenantOrganizationsMap + this.randomTenants, + this.randomTenantOrganizationsMap ) ); @@ -2091,7 +2022,7 @@ export class SeedDataService { 'Random Candidate Criterion Ratings', createRandomCandidateCriterionRating( this.dataSource, - tenants, + this.randomTenants, tenantCandidatesMap ) ); @@ -2100,9 +2031,9 @@ export class SeedDataService { 'Random Event Types', createRandomEventType( this.dataSource, - tenants, - tenantOrganizationsMap, - organizationEmployeesMap + this.randomTenants, + this.randomTenantOrganizationsMap, + this.randomOrganizationEmployeesMap ) ); @@ -2111,7 +2042,7 @@ export class SeedDataService { createRandomTimesheet( this.dataSource, this.configService.config, - tenants + this.randomTenants ) ); diff --git a/packages/core/src/core/seeds/utils.ts b/packages/core/src/core/seeds/utils.ts index 26f7ec23521..680bb386ef2 100644 --- a/packages/core/src/core/seeds/utils.ts +++ b/packages/core/src/core/seeds/utils.ts @@ -69,3 +69,22 @@ export async function cleanAssets(config: Partial, dest ); }); } + +/** + * Takes an email string, converts it to lowercase, and appends a postfix "_ever_testing" before the "@" symbol. + * + * @param email The email address to modify. + * @param postfix The postfix to append (default is "_ever_testing"). + * @returns The modified email address with the postfix appended before the "@" symbol. + */ +export function getEmailWithPostfix(email: string, postfix = '_ever_testing'): string { + const atIndex = email.indexOf('@'); + if (atIndex === -1) { + // If "@" symbol not found, return original email + return email; + } + const localPart = email.slice(0, atIndex); // Extract local part before "@" + const domainPart = email.slice(atIndex); // Extract domain part including "@" + const lowercaseLocalPart = localPart.toLowerCase(); + return lowercaseLocalPart + postfix + domainPart; +} diff --git a/packages/core/src/core/utils.ts b/packages/core/src/core/utils.ts index 46f4b4f8a11..ae2de4dcb5e 100644 --- a/packages/core/src/core/utils.ts +++ b/packages/core/src/core/utils.ts @@ -239,9 +239,7 @@ export function getDateRangeFormat( end: end.toDate() }; default: - throw Error( - `cannot get date range due to unsupported database type: ${getConfig().dbConnectionOptions.type}` - ); + throw Error(`cannot get date range due to unsupported database type: ${getConfig().dbConnectionOptions.type}`); } } diff --git a/packages/core/src/database/connection-entity-manager.ts b/packages/core/src/database/connection-entity-manager.ts new file mode 100644 index 00000000000..1b4d9f5f595 --- /dev/null +++ b/packages/core/src/database/connection-entity-manager.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager } from '@nestjs/typeorm'; +import { + DataSource, + EntityManager, + EntitySchema, + ObjectLiteral, + ObjectType, + Repository +} from 'typeorm'; + +@Injectable() +export class ConnectionEntityManager { + + constructor( + @InjectEntityManager() private entityManager: EntityManager + ) { } + + /** + * Retrieves the raw EntityManager instance. + * + * @returns The raw EntityManager instance. + */ + get rawEntityManager(): EntityManager { + return this.entityManager; + } + + /** + * Retrieves the raw connection from the EntityManager. + * + * @returns The raw connection from the EntityManager. + */ + get rawConnection(): DataSource { + return this.entityManager.connection; + } + + /** + * Returns a TypeORM repository for the specified target entity. + * + * @param target The target entity type or entity schema for which to retrieve the repository. + * @returns The TypeORM repository for the specified target entity. + */ + getRepository( + target: ObjectType | EntitySchema | string, + ): Repository; + + /** + * Returns a TypeORM repository based on the provided target. + * + * @param target The target entity type or entity schema for which to retrieve the repository. + * @returns The TypeORM repository for the specified target entity. + */ + getRepository( + target?: ObjectType | EntitySchema | string, + ): Repository { + return this.rawEntityManager.getRepository(target!); + } +} diff --git a/packages/core/src/database/database.module.ts b/packages/core/src/database/database.module.ts index ba54fbd9a5a..b0e6d0b01a6 100644 --- a/packages/core/src/database/database.module.ts +++ b/packages/core/src/database/database.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { KnexModule } from 'nest-knexjs'; import { ConfigModule, ConfigService } from '@gauzy/config'; +import { ConnectionEntityManager } from './connection-entity-manager'; /** * Import and provide base typeorm related classes. @@ -53,9 +54,7 @@ import { ConfigModule, ConfigService } from '@gauzy/config'; } }) ], - exports: [ - TypeOrmModule, - MikroOrmModule - ] + providers: [ConnectionEntityManager], + exports: [TypeOrmModule, MikroOrmModule, ConnectionEntityManager] }) export class DatabaseModule { } diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index 2801d9a9d52..b6a394a1ab6 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -1 +1,3 @@ -export * from './migration-executor'; +export { createMigration, generateMigration, revertLastDatabaseMigration, runDatabaseMigrations } from './migration-executor'; +export { ConnectionEntityManager } from './connection-entity-manager'; +export { prepareSQLQuery } from './database.helper'; diff --git a/packages/core/src/database/migrations/1713187612530-AlterEmployeeCustomEntityFields.ts b/packages/core/src/database/migrations/1713187612530-AlterEmployeeCustomEntityFields.ts new file mode 100644 index 00000000000..f4fd6650a23 --- /dev/null +++ b/packages/core/src/database/migrations/1713187612530-AlterEmployeeCustomEntityFields.ts @@ -0,0 +1,147 @@ + +import { MigrationInterface, QueryRunner } from "typeorm"; +import { yellow } from "chalk"; +import { DatabaseTypeEnum } from "@gauzy/config"; + +export class AlterEmployeeCustomEntityFields1713187612530 implements MigrationInterface { + + name = 'AlterEmployeeCustomEntityFields1713187612530'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(yellow(this.name + ' start running!')); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "employee" ADD "__fix_relational_custom_fields__" boolean`); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "employee" DROP COLUMN "__fix_relational_custom_fields__"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_175b7be641928a31521224daa8"`); + await queryRunner.query(`DROP INDEX "IDX_510cb87f5da169e57e694d1a5c"`); + await queryRunner.query(`DROP INDEX "IDX_4b3303a6b7eb92d237a4379734"`); + await queryRunner.query(`DROP INDEX "IDX_c6a48286f3aa8ae903bee0d1e7"`); + await queryRunner.query(`DROP INDEX "IDX_96dfbcaa2990df01fe5bb39ccc"`); + await queryRunner.query(`DROP INDEX "IDX_f4b0d329c4a3cf79ffe9d56504"`); + await queryRunner.query(`DROP INDEX "IDX_1c0c1370ecd98040259625e17e"`); + await queryRunner.query(`DROP INDEX "IDX_5e719204dcafa8d6b2ecdeda13"`); + await queryRunner.query(`CREATE TABLE "temporary_employee" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "valueDate" datetime, "isActive" boolean DEFAULT (1), "short_description" varchar(200), "description" varchar, "startedWorkOn" datetime, "endWork" datetime, "payPeriod" varchar, "billRateValue" integer, "billRateCurrency" varchar, "reWeeklyLimit" integer, "offerDate" datetime, "acceptDate" datetime, "rejectDate" datetime, "employeeLevel" varchar(500), "anonymousBonus" boolean, "averageIncome" numeric, "averageBonus" numeric, "totalWorkHours" numeric DEFAULT (0), "averageExpenses" numeric, "show_anonymous_bonus" boolean, "show_average_bonus" boolean, "show_average_expenses" boolean, "show_average_income" boolean, "show_billrate" boolean, "show_payperiod" boolean, "show_start_work_on" boolean, "isJobSearchActive" boolean, "linkedInUrl" varchar, "facebookUrl" varchar, "instagramUrl" varchar, "twitterUrl" varchar, "githubUrl" varchar, "gitlabUrl" varchar, "upworkUrl" varchar, "stackoverflowUrl" varchar, "isVerified" boolean, "isVetted" boolean, "totalJobs" numeric, "jobSuccess" numeric, "profile_link" varchar, "userId" varchar NOT NULL, "contactId" varchar, "organizationPositionId" varchar, "isTrackingEnabled" boolean DEFAULT (0), "deletedAt" datetime, "allowScreenshotCapture" boolean NOT NULL DEFAULT (1), "upworkId" varchar, "linkedInId" varchar, "isOnline" boolean DEFAULT (0), "isTrackingTime" boolean DEFAULT (0), "minimumBillingRate" integer, "isAway" boolean DEFAULT (0), "isArchived" boolean DEFAULT (0), "__fix_relational_custom_fields__" boolean, CONSTRAINT "REL_f4b0d329c4a3cf79ffe9d56504" UNIQUE ("userId"), CONSTRAINT "REL_1c0c1370ecd98040259625e17e" UNIQUE ("contactId"), CONSTRAINT "FK_4b3303a6b7eb92d237a4379734e" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_c6a48286f3aa8ae903bee0d1e72" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_f4b0d329c4a3cf79ffe9d565047" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_1c0c1370ecd98040259625e17e2" FOREIGN KEY ("contactId") REFERENCES "contact" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_5e719204dcafa8d6b2ecdeda130" FOREIGN KEY ("organizationPositionId") REFERENCES "organization_position" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_employee"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived" FROM "employee"`); + await queryRunner.query(`DROP TABLE "employee"`); + await queryRunner.query(`ALTER TABLE "temporary_employee" RENAME TO "employee"`); + await queryRunner.query(`CREATE INDEX "IDX_175b7be641928a31521224daa8" ON "employee" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_510cb87f5da169e57e694d1a5c" ON "employee" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_4b3303a6b7eb92d237a4379734" ON "employee" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_c6a48286f3aa8ae903bee0d1e7" ON "employee" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_96dfbcaa2990df01fe5bb39ccc" ON "employee" ("profile_link") `); + await queryRunner.query(`CREATE INDEX "IDX_f4b0d329c4a3cf79ffe9d56504" ON "employee" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_1c0c1370ecd98040259625e17e" ON "employee" ("contactId") `); + await queryRunner.query(`CREATE INDEX "IDX_5e719204dcafa8d6b2ecdeda13" ON "employee" ("organizationPositionId") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_5e719204dcafa8d6b2ecdeda13"`); + await queryRunner.query(`DROP INDEX "IDX_1c0c1370ecd98040259625e17e"`); + await queryRunner.query(`DROP INDEX "IDX_f4b0d329c4a3cf79ffe9d56504"`); + await queryRunner.query(`DROP INDEX "IDX_96dfbcaa2990df01fe5bb39ccc"`); + await queryRunner.query(`DROP INDEX "IDX_c6a48286f3aa8ae903bee0d1e7"`); + await queryRunner.query(`DROP INDEX "IDX_4b3303a6b7eb92d237a4379734"`); + await queryRunner.query(`DROP INDEX "IDX_510cb87f5da169e57e694d1a5c"`); + await queryRunner.query(`DROP INDEX "IDX_175b7be641928a31521224daa8"`); + await queryRunner.query(`ALTER TABLE "employee" RENAME TO "temporary_employee"`); + await queryRunner.query(`CREATE TABLE "employee" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "valueDate" datetime, "isActive" boolean DEFAULT (1), "short_description" varchar(200), "description" varchar, "startedWorkOn" datetime, "endWork" datetime, "payPeriod" varchar, "billRateValue" integer, "billRateCurrency" varchar, "reWeeklyLimit" integer, "offerDate" datetime, "acceptDate" datetime, "rejectDate" datetime, "employeeLevel" varchar(500), "anonymousBonus" boolean, "averageIncome" numeric, "averageBonus" numeric, "totalWorkHours" numeric DEFAULT (0), "averageExpenses" numeric, "show_anonymous_bonus" boolean, "show_average_bonus" boolean, "show_average_expenses" boolean, "show_average_income" boolean, "show_billrate" boolean, "show_payperiod" boolean, "show_start_work_on" boolean, "isJobSearchActive" boolean, "linkedInUrl" varchar, "facebookUrl" varchar, "instagramUrl" varchar, "twitterUrl" varchar, "githubUrl" varchar, "gitlabUrl" varchar, "upworkUrl" varchar, "stackoverflowUrl" varchar, "isVerified" boolean, "isVetted" boolean, "totalJobs" numeric, "jobSuccess" numeric, "profile_link" varchar, "userId" varchar NOT NULL, "contactId" varchar, "organizationPositionId" varchar, "isTrackingEnabled" boolean DEFAULT (0), "deletedAt" datetime, "allowScreenshotCapture" boolean NOT NULL DEFAULT (1), "upworkId" varchar, "linkedInId" varchar, "isOnline" boolean DEFAULT (0), "isTrackingTime" boolean DEFAULT (0), "minimumBillingRate" integer, "isAway" boolean DEFAULT (0), "isArchived" boolean DEFAULT (0), CONSTRAINT "REL_f4b0d329c4a3cf79ffe9d56504" UNIQUE ("userId"), CONSTRAINT "REL_1c0c1370ecd98040259625e17e" UNIQUE ("contactId"), CONSTRAINT "FK_4b3303a6b7eb92d237a4379734e" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_c6a48286f3aa8ae903bee0d1e72" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_f4b0d329c4a3cf79ffe9d565047" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_1c0c1370ecd98040259625e17e2" FOREIGN KEY ("contactId") REFERENCES "contact" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_5e719204dcafa8d6b2ecdeda130" FOREIGN KEY ("organizationPositionId") REFERENCES "organization_position" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "employee"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived" FROM "temporary_employee"`); + await queryRunner.query(`DROP TABLE "temporary_employee"`); + await queryRunner.query(`CREATE INDEX "IDX_5e719204dcafa8d6b2ecdeda13" ON "employee" ("organizationPositionId") `); + await queryRunner.query(`CREATE INDEX "IDX_1c0c1370ecd98040259625e17e" ON "employee" ("contactId") `); + await queryRunner.query(`CREATE INDEX "IDX_f4b0d329c4a3cf79ffe9d56504" ON "employee" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_96dfbcaa2990df01fe5bb39ccc" ON "employee" ("profile_link") `); + await queryRunner.query(`CREATE INDEX "IDX_c6a48286f3aa8ae903bee0d1e7" ON "employee" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_4b3303a6b7eb92d237a4379734" ON "employee" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_510cb87f5da169e57e694d1a5c" ON "employee" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_175b7be641928a31521224daa8" ON "employee" ("isArchived") `); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`employee\` ADD \`__fix_relational_custom_fields__\` tinyint NULL`); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`employee\` DROP COLUMN \`__fix_relational_custom_fields__\``); + } +} diff --git a/packages/core/src/database/migrations/1713275626299-AlterTagCustomEntityFields.ts b/packages/core/src/database/migrations/1713275626299-AlterTagCustomEntityFields.ts new file mode 100644 index 00000000000..c484e58eb41 --- /dev/null +++ b/packages/core/src/database/migrations/1713275626299-AlterTagCustomEntityFields.ts @@ -0,0 +1,173 @@ + +import { MigrationInterface, QueryRunner } from "typeorm"; +import { yellow } from "chalk"; +import { DatabaseTypeEnum } from "@gauzy/config"; + +export class AlterTagCustomEntityFields1713275626299 implements MigrationInterface { + + name = 'AlterTagCustomEntityFields1713275626299'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(yellow(this.name + ' start running!')); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tag_proposal" DROP CONSTRAINT "FK_451853704de278eef61a37fa7a6"`); + await queryRunner.query(`ALTER TABLE "tag" ADD "__fix_relational_custom_fields__" boolean`); + await queryRunner.query(`ALTER TABLE "tag_proposal" ADD CONSTRAINT "FK_451853704de278eef61a37fa7a6" FOREIGN KEY ("tagId") REFERENCES "tag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tag_proposal" DROP CONSTRAINT "FK_451853704de278eef61a37fa7a6"`); + await queryRunner.query(`ALTER TABLE "tag" DROP COLUMN "__fix_relational_custom_fields__"`); + await queryRunner.query(`ALTER TABLE "tag_proposal" ADD CONSTRAINT "FK_451853704de278eef61a37fa7a6" FOREIGN KEY ("tagId") REFERENCES "tag"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_451853704de278eef61a37fa7a"`); + await queryRunner.query(`DROP INDEX "IDX_3f55851a03524e567594d50774"`); + await queryRunner.query(`CREATE TABLE "temporary_tag_proposal" ("proposalId" varchar NOT NULL, "tagId" varchar NOT NULL, CONSTRAINT "FK_3f55851a03524e567594d507744" FOREIGN KEY ("proposalId") REFERENCES "proposal" ("id") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY ("proposalId", "tagId"))`); + await queryRunner.query(`INSERT INTO "temporary_tag_proposal"("proposalId", "tagId") SELECT "proposalId", "tagId" FROM "tag_proposal"`); + await queryRunner.query(`DROP TABLE "tag_proposal"`); + await queryRunner.query(`ALTER TABLE "temporary_tag_proposal" RENAME TO "tag_proposal"`); + await queryRunner.query(`CREATE INDEX "IDX_451853704de278eef61a37fa7a" ON "tag_proposal" ("tagId") `); + await queryRunner.query(`CREATE INDEX "IDX_3f55851a03524e567594d50774" ON "tag_proposal" ("proposalId") `); + await queryRunner.query(`DROP INDEX "IDX_58876ee26a90170551027459bf"`); + await queryRunner.query(`DROP INDEX "IDX_1f22c73374bcca1ea84a4dca59"`); + await queryRunner.query(`DROP INDEX "IDX_c2f6bec0b39eaa3a6d90903ae9"`); + await queryRunner.query(`DROP INDEX "IDX_b08dd29fb6a8acdf83c83d8988"`); + await queryRunner.query(`DROP INDEX "IDX_49746602acc4e5e8721062b69e"`); + await queryRunner.query(`CREATE TABLE "temporary_tag" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "name" varchar NOT NULL, "description" varchar, "color" varchar NOT NULL, "isSystem" boolean NOT NULL DEFAULT (0), "icon" varchar, "organizationTeamId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "deletedAt" datetime, "textColor" varchar, "__fix_relational_custom_fields__" boolean, CONSTRAINT "FK_c2f6bec0b39eaa3a6d90903ae99" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_b08dd29fb6a8acdf83c83d8988f" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_49746602acc4e5e8721062b69ec" FOREIGN KEY ("organizationTeamId") REFERENCES "organization_team" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_tag"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "description", "color", "isSystem", "icon", "organizationTeamId", "isActive", "isArchived", "deletedAt", "textColor") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "description", "color", "isSystem", "icon", "organizationTeamId", "isActive", "isArchived", "deletedAt", "textColor" FROM "tag"`); + await queryRunner.query(`DROP TABLE "tag"`); + await queryRunner.query(`ALTER TABLE "temporary_tag" RENAME TO "tag"`); + await queryRunner.query(`CREATE INDEX "IDX_58876ee26a90170551027459bf" ON "tag" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_1f22c73374bcca1ea84a4dca59" ON "tag" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_c2f6bec0b39eaa3a6d90903ae9" ON "tag" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_b08dd29fb6a8acdf83c83d8988" ON "tag" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_49746602acc4e5e8721062b69e" ON "tag" ("organizationTeamId") `); + await queryRunner.query(`CREATE TABLE "temporary_tag_proposal" ("proposalId" varchar NOT NULL, "tagId" varchar NOT NULL, CONSTRAINT "FK_3f55851a03524e567594d507744" FOREIGN KEY ("proposalId") REFERENCES "proposal" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_451853704de278eef61a37fa7a6" FOREIGN KEY ("tagId") REFERENCES "tag" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, PRIMARY KEY ("proposalId", "tagId"))`); + await queryRunner.query(`INSERT INTO "temporary_tag_proposal"("proposalId", "tagId") SELECT "proposalId", "tagId" FROM "tag_proposal"`); + await queryRunner.query(`DROP TABLE "tag_proposal"`); + await queryRunner.query(`ALTER TABLE "temporary_tag_proposal" RENAME TO "tag_proposal"`); + await queryRunner.query(`CREATE INDEX "IDX_451853704de278eef61a37fa7a" ON "tag_proposal" ("tagId") `); + await queryRunner.query(`CREATE INDEX "IDX_3f55851a03524e567594d50774" ON "tag_proposal" ("proposalId") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_3f55851a03524e567594d50774"`); + await queryRunner.query(`DROP INDEX "IDX_451853704de278eef61a37fa7a"`); + await queryRunner.query(`ALTER TABLE "tag_proposal" RENAME TO "temporary_tag_proposal"`); + await queryRunner.query(`CREATE TABLE "tag_proposal" ("proposalId" varchar NOT NULL, "tagId" varchar NOT NULL, CONSTRAINT "FK_3f55851a03524e567594d507744" FOREIGN KEY ("proposalId") REFERENCES "proposal" ("id") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY ("proposalId", "tagId"))`); + await queryRunner.query(`INSERT INTO "tag_proposal"("proposalId", "tagId") SELECT "proposalId", "tagId" FROM "temporary_tag_proposal"`); + await queryRunner.query(`DROP TABLE "temporary_tag_proposal"`); + await queryRunner.query(`CREATE INDEX "IDX_3f55851a03524e567594d50774" ON "tag_proposal" ("proposalId") `); + await queryRunner.query(`CREATE INDEX "IDX_451853704de278eef61a37fa7a" ON "tag_proposal" ("tagId") `); + await queryRunner.query(`DROP INDEX "IDX_49746602acc4e5e8721062b69e"`); + await queryRunner.query(`DROP INDEX "IDX_b08dd29fb6a8acdf83c83d8988"`); + await queryRunner.query(`DROP INDEX "IDX_c2f6bec0b39eaa3a6d90903ae9"`); + await queryRunner.query(`DROP INDEX "IDX_1f22c73374bcca1ea84a4dca59"`); + await queryRunner.query(`DROP INDEX "IDX_58876ee26a90170551027459bf"`); + await queryRunner.query(`ALTER TABLE "tag" RENAME TO "temporary_tag"`); + await queryRunner.query(`CREATE TABLE "tag" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "name" varchar NOT NULL, "description" varchar, "color" varchar NOT NULL, "isSystem" boolean NOT NULL DEFAULT (0), "icon" varchar, "organizationTeamId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "deletedAt" datetime, "textColor" varchar, CONSTRAINT "FK_c2f6bec0b39eaa3a6d90903ae99" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_b08dd29fb6a8acdf83c83d8988f" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_49746602acc4e5e8721062b69ec" FOREIGN KEY ("organizationTeamId") REFERENCES "organization_team" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "tag"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "description", "color", "isSystem", "icon", "organizationTeamId", "isActive", "isArchived", "deletedAt", "textColor") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "description", "color", "isSystem", "icon", "organizationTeamId", "isActive", "isArchived", "deletedAt", "textColor" FROM "temporary_tag"`); + await queryRunner.query(`DROP TABLE "temporary_tag"`); + await queryRunner.query(`CREATE INDEX "IDX_49746602acc4e5e8721062b69e" ON "tag" ("organizationTeamId") `); + await queryRunner.query(`CREATE INDEX "IDX_b08dd29fb6a8acdf83c83d8988" ON "tag" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_c2f6bec0b39eaa3a6d90903ae9" ON "tag" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_1f22c73374bcca1ea84a4dca59" ON "tag" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_58876ee26a90170551027459bf" ON "tag" ("isArchived") `); + await queryRunner.query(`DROP INDEX "IDX_3f55851a03524e567594d50774"`); + await queryRunner.query(`DROP INDEX "IDX_451853704de278eef61a37fa7a"`); + await queryRunner.query(`ALTER TABLE "tag_proposal" RENAME TO "temporary_tag_proposal"`); + await queryRunner.query(`CREATE TABLE "tag_proposal" ("proposalId" varchar NOT NULL, "tagId" varchar NOT NULL, CONSTRAINT "FK_451853704de278eef61a37fa7a6" FOREIGN KEY ("tagId") REFERENCES "tag" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3f55851a03524e567594d507744" FOREIGN KEY ("proposalId") REFERENCES "proposal" ("id") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY ("proposalId", "tagId"))`); + await queryRunner.query(`INSERT INTO "tag_proposal"("proposalId", "tagId") SELECT "proposalId", "tagId" FROM "temporary_tag_proposal"`); + await queryRunner.query(`DROP TABLE "temporary_tag_proposal"`); + await queryRunner.query(`CREATE INDEX "IDX_3f55851a03524e567594d50774" ON "tag_proposal" ("proposalId") `); + await queryRunner.query(`CREATE INDEX "IDX_451853704de278eef61a37fa7a" ON "tag_proposal" ("tagId") `); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`tag_proposal\` DROP FOREIGN KEY \`FK_451853704de278eef61a37fa7a6\``); + await queryRunner.query(`ALTER TABLE \`tag\` ADD \`__fix_relational_custom_fields__\` tinyint NULL`); + await queryRunner.query(`ALTER TABLE \`tag_proposal\` ADD CONSTRAINT \`FK_451853704de278eef61a37fa7a6\` FOREIGN KEY (\`tagId\`) REFERENCES \`tag\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`tag_proposal\` DROP FOREIGN KEY \`FK_451853704de278eef61a37fa7a6\``); + await queryRunner.query(`ALTER TABLE \`tag\` DROP COLUMN \`__fix_relational_custom_fields__\``); + await queryRunner.query(`ALTER TABLE \`tag_proposal\` ADD CONSTRAINT \`FK_451853704de278eef61a37fa7a6\` FOREIGN KEY (\`tagId\`) REFERENCES \`tag\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/core/src/deal/deal.module.ts b/packages/core/src/deal/deal.module.ts index 273534cd5f3..51bafa79049 100644 --- a/packages/core/src/deal/deal.module.ts +++ b/packages/core/src/deal/deal.module.ts @@ -6,16 +6,19 @@ import { Deal } from './deal.entity'; import { DealController } from './deal.controller'; import { DealService } from './deal.service'; import { RolePermissionModule } from '../role-permission/role-permission.module'; +import { TypeOrmDealRepository } from './repository/type-orm-deal.repository'; @Module({ imports: [ - RouterModule.register([{ path: '/deals', module: DealModule }]), + RouterModule.register([ + { path: '/deals', module: DealModule } + ]), TypeOrmModule.forFeature([Deal]), MikroOrmModule.forFeature([Deal]), RolePermissionModule ], controllers: [DealController], - providers: [DealService], - exports: [DealService] + providers: [DealService, TypeOrmDealRepository], + exports: [DealService, TypeOrmDealRepository] }) export class DealModule { } diff --git a/packages/core/src/deal/deal.service.ts b/packages/core/src/deal/deal.service.ts index 92c992006e0..15cd6813417 100644 --- a/packages/core/src/deal/deal.service.ts +++ b/packages/core/src/deal/deal.service.ts @@ -1,4 +1,3 @@ -import { InjectRepository } from '@nestjs/typeorm'; import { Injectable } from '@nestjs/common'; import { TenantAwareCrudService } from './../core/crud'; import { Deal } from './deal.entity'; @@ -8,10 +7,8 @@ import { MikroOrmDealRepository } from './repository/mikro-orm-deal.repository'; @Injectable() export class DealService extends TenantAwareCrudService { constructor( - @InjectRepository(Deal) - typeOrmDealRepository: TypeOrmDealRepository, - - mikroOrmDealRepository: MikroOrmDealRepository + readonly typeOrmDealRepository: TypeOrmDealRepository, + readonly mikroOrmDealRepository: MikroOrmDealRepository ) { super(typeOrmDealRepository, mikroOrmDealRepository); } diff --git a/packages/core/src/deal/repository/index.ts b/packages/core/src/deal/repository/index.ts new file mode 100644 index 00000000000..d1487019b18 --- /dev/null +++ b/packages/core/src/deal/repository/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-deal.repository'; +export * from './type-orm-deal.repository'; diff --git a/packages/core/src/email-history/email-history.seed.ts b/packages/core/src/email-history/email-history.seed.ts index 9e3dddbb051..16a645a3b74 100644 --- a/packages/core/src/email-history/email-history.seed.ts +++ b/packages/core/src/email-history/email-history.seed.ts @@ -2,7 +2,8 @@ import { DataSource, ILike, Not } from 'typeorm'; import { faker } from '@faker-js/faker'; import { EmailHistory } from './email-history.entity'; import { IEmailHistory, IEmailTemplate, IOrganization, ITenant, IUser } from '@gauzy/contracts'; -import { EmailTemplate, User } from './../core/entities/internal'; +import { EmailTemplate, User } from '../core/entities/internal'; +import { getEmailWithPostfix } from '../core/seeds/utils'; export const createDefaultEmailSent = async ( dataSource: DataSource, @@ -60,6 +61,9 @@ export const createRandomEmailSent = async ( return sentEmails; }; +/** + * + */ const dataOperation = async ( dataSource: DataSource, sentEmails: IEmailHistory[], @@ -72,7 +76,7 @@ const dataOperation = async ( for (let i = 0; i < noOfEmailsPerOrganization; i++) { const sentEmail = new EmailHistory(); sentEmail.organization = organization; - sentEmail.email = faker.internet.exampleEmail(); + sentEmail.email = getEmailWithPostfix(faker.internet.exampleEmail()); sentEmail.emailTemplate = faker.helpers.arrayElement(emailTemplates); sentEmail.name = sentEmail.emailTemplate.name.split('/')[0]; sentEmail.content = sentEmail.emailTemplate.hbs; diff --git a/packages/core/src/employee-job-preset/commands/handlers/save-employee-preset.handler.ts b/packages/core/src/employee-job-preset/commands/handlers/save-employee-preset.handler.ts deleted file mode 100644 index 92fd965a11e..00000000000 --- a/packages/core/src/employee-job-preset/commands/handlers/save-employee-preset.handler.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { GauzyAIService } from '@gauzy/integration-ai'; -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { InjectRepository } from '@nestjs/typeorm'; -import { In } from 'typeorm'; -import { Employee } from '../../../employee/employee.entity'; -import { EmployeeUpworkJobsSearchCriterion } from '../../employee-upwork-jobs-search-criterion.entity'; -import { JobPreset } from '../../job-preset.entity'; -import { SaveEmployeePresetCommand } from '../save-employee-preset.command'; -import { TypeOrmJobPresetRepository } from '../../repository/type-orm-job-preset.repository'; -import { TypeOrmEmployeeUpworkJobsSearchCriterionRepository } from '../../repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository'; -import { TypeOrmEmployeeRepository } from '../../../employee/repository/type-orm-employee.repository'; - -@CommandHandler(SaveEmployeePresetCommand) -export class SaveEmployeePresetHandler implements ICommandHandler { - constructor( - private readonly gauzyAIService: GauzyAIService, - - @InjectRepository(JobPreset) - private readonly typeOrmJobPresetRepository: TypeOrmJobPresetRepository, - - @InjectRepository(Employee) - private readonly typeOrmEmployeeRepository: TypeOrmEmployeeRepository, - - @InjectRepository(EmployeeUpworkJobsSearchCriterion) - private readonly typeOrmEmployeeUpworkJobsSearchCriterionRepository: TypeOrmEmployeeUpworkJobsSearchCriterionRepository - ) { } - - public async execute( - command: SaveEmployeePresetCommand - ): Promise { - const { input } = command; - const employee = await this.typeOrmEmployeeRepository.findOne({ - where: { - id: input.employeeId - }, - relations: { - jobPresets: true, - organization: true - } - }); - const jobPreset = await this.typeOrmJobPresetRepository.findOne({ - where: { - id: In(input.jobPresetIds) - }, - relations: { - jobPresetCriterions: true - } - }); - const employeeCriterions = jobPreset.jobPresetCriterions.map((item) => { - return new EmployeeUpworkJobsSearchCriterion({ - ...item, - employeeId: input.employeeId - }); - }); - - employee.jobPresets = input.jobPresetIds.map( - (id) => new JobPreset({ id }) - ); - this.typeOrmEmployeeRepository.save(employee); - - await this.typeOrmEmployeeUpworkJobsSearchCriterionRepository.delete({ - employeeId: input.employeeId - }); - - await this.typeOrmEmployeeUpworkJobsSearchCriterionRepository.save( - employeeCriterions - ); - - this.gauzyAIService.syncGauzyEmployeeJobSearchCriteria( - employee, - employeeCriterions - ); - - return employeeCriterions; - } -} diff --git a/packages/core/src/employee-job-preset/dto/job-preset-query.dto.ts b/packages/core/src/employee-job-preset/dto/job-preset-query.dto.ts deleted file mode 100644 index 9344608b93d..00000000000 --- a/packages/core/src/employee-job-preset/dto/job-preset-query.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IntersectionType, PartialType, PickType } from '@nestjs/swagger'; -import { IGetJobPresetInput } from '@gauzy/contracts'; -import { TenantOrganizationBaseDTO } from './../../core/dto'; -import { EmployeeFeatureDTO } from './../../employee/dto'; - -export class JobPresetQueryDTO - extends IntersectionType(TenantOrganizationBaseDTO, PartialType(PickType(EmployeeFeatureDTO, ['employeeId']))) - implements IGetJobPresetInput {} diff --git a/packages/core/src/employee-job-preset/job-preset.service.ts b/packages/core/src/employee-job-preset/job-preset.service.ts deleted file mode 100644 index ad393d37519..00000000000 --- a/packages/core/src/employee-job-preset/job-preset.service.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { CommandBus } from '@nestjs/cqrs'; -import { SelectQueryBuilder } from 'typeorm'; -import { - IEmployeePresetInput, - IGetJobPresetCriterionInput, - IGetJobPresetInput, - IGetMatchingCriterions, - IJobPreset, - IMatchingCriterions -} from '@gauzy/contracts'; -import { isPostgres } from '@gauzy/config'; -import { TenantAwareCrudService } from './../core/crud'; -import { RequestContext } from './../core/context'; -import { JobPresetUpworkJobSearchCriterion } from './job-preset-upwork-job-search-criterion.entity'; -import { EmployeeUpworkJobsSearchCriterion } from './employee-upwork-jobs-search-criterion.entity'; -import { JobPreset } from './job-preset.entity'; -import { Employee } from '../employee/employee.entity'; -import { - CreateJobPresetCommand, - SaveEmployeeCriterionCommand, - SaveEmployeePresetCommand, - SavePresetCriterionCommand -} from './commands'; -import { isNotEmpty } from 'class-validator'; -import { prepareSQLQuery as p } from './../database/database.helper'; -import { TypeOrmJobPresetRepository } from './repository/type-orm-job-preset.repository'; -import { MikroOrmJobPresetRepository } from './repository/mikro-orm-job-preset.repository'; -import { TypeOrmJobPresetUpworkJobSearchCriterionRepository } from './repository/type-orm-job-preset-upwork-job-search-criterion.repository'; -import { TypeOrmEmployeeUpworkJobsSearchCriterionRepository } from './repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository'; -import { TypeOrmEmployeeRepository } from './../employee/repository/type-orm-employee.repository'; - -@Injectable() -export class JobPresetService extends TenantAwareCrudService { - constructor( - private readonly commandBus: CommandBus, - - @InjectRepository(JobPreset) - typeOrmJobPresetRepository: TypeOrmJobPresetRepository, - - mikroOrmJobPresetRepository: MikroOrmJobPresetRepository, - - @InjectRepository(JobPresetUpworkJobSearchCriterion) - private typeOrmJobPresetUpworkJobSearchCriterionRepository: TypeOrmJobPresetUpworkJobSearchCriterionRepository, - - @InjectRepository(EmployeeUpworkJobsSearchCriterion) - private typeOrmEmployeeUpworkJobsSearchCriterionRepository: TypeOrmEmployeeUpworkJobsSearchCriterionRepository, - - @InjectRepository(Employee) - private typeOrmEmployeeRepository: TypeOrmEmployeeRepository - ) { - super(typeOrmJobPresetRepository, mikroOrmJobPresetRepository); - } - - /** - * - * @param request - * @returns - */ - public async getAll(request?: IGetJobPresetInput) { - const tenantId = RequestContext.currentTenantId() || request.tenantId; - const { organizationId, search, employeeId } = request; - const likeOperator = isPostgres() ? 'ILIKE' : 'LIKE'; - - - const query = this.typeOrmRepository.createQueryBuilder('job_preset'); - query.setFindOptions({ - join: { - alias: 'job_preset', - leftJoin: { - employees: 'job_preset.employees' - } - }, - relations: { - jobPresetCriterions: true - }, - order: { - name: 'ASC' - } - }); - query.where((qb: SelectQueryBuilder) => { - qb.andWhere(p(`"${qb.alias}"."tenantId" = :tenantId`), { tenantId }); - - if (isNotEmpty(organizationId)) { - qb.andWhere(p(`"${qb.alias}"."organizationId" = :organizationId`), { - organizationId - }); - } - if (isNotEmpty(search)) { - qb.andWhere(p(`"${query.alias}"."name" ${likeOperator} :search`), { - search: `%${search}%` - }); - } - if (isNotEmpty(employeeId)) { - qb.andWhere(p(`"employees"."id" = :employeeId`), { - employeeId - }); - } - }); - return await query.getMany(); - } - - public async get(id: string, request?: IGetJobPresetCriterionInput) { - const query = this.typeOrmRepository.createQueryBuilder(); - query.leftJoinAndSelect(`${query.alias}.jobPresetCriterions`, 'jobPresetCriterions'); - if (request.employeeId) { - query.leftJoinAndSelect( - `${query.alias}.employeeCriterions`, - 'employeeCriterions', - 'employeeCriterions.employeeId = :employeeId', - { employeeId: request.employeeId } - ); - } - query.andWhere(`${query.alias}.id = :id`, { id }); - - return query.getOne(); - } - - /** - * - * @param presetId - * @returns - */ - public getJobPresetCriterion(presetId: string) { - return this.typeOrmJobPresetUpworkJobSearchCriterionRepository.findBy({ - jobPresetId: presetId - }); - } - - /** - * - * @param input - * @returns - */ - public getEmployeeCriterion(input: IGetMatchingCriterions) { - return this.typeOrmEmployeeUpworkJobsSearchCriterionRepository.findBy({ - ...(input.jobPresetId ? { jobPresetId: input.jobPresetId } : {}), - employeeId: input.employeeId - }); - } - - /** - * - * @param request - * @returns - */ - public async createJobPreset(request?: IJobPreset) { - return this.commandBus.execute( - new CreateJobPresetCommand(request) - ); - } - - /** - * - * @param request - * @returns - */ - async saveJobPresetCriterion(request: IMatchingCriterions) { - return this.commandBus.execute( - new SavePresetCriterionCommand(request) - ); - } - - /** - * - * @param request - * @returns - */ - async saveEmployeeCriterion(request: IMatchingCriterions) { - return this.commandBus.execute( - new SaveEmployeeCriterionCommand(request) - ); - } - - /** - * - * @param employeeId - * @returns - */ - async getEmployeePreset(employeeId: string) { - const employee = await this.typeOrmEmployeeRepository.findOne({ - where: { - id: employeeId - }, - relations: { - jobPresets: true - } - }); - return employee.jobPresets; - } - - /** - * - * @param request - * @returns - */ - async saveEmployeePreset(request: IEmployeePresetInput) { - return this.commandBus.execute( - new SaveEmployeePresetCommand(request) - ); - } - - /** - * - * @param creationId - * @param employeeId - * @returns - */ - deleteEmployeeCriterion(creationId: string, employeeId: string) { - return this.typeOrmEmployeeUpworkJobsSearchCriterionRepository.delete({ - id: creationId, - employeeId: employeeId - }); - } - - /** - * - * @param creationId - * @returns - */ - deleteJobPresetCriterion(creationId: string) { - return this.typeOrmJobPresetUpworkJobSearchCriterionRepository.delete(creationId); - } -} diff --git a/packages/core/src/employee-job-preset/job-search-category/job-search-category.seed.ts b/packages/core/src/employee-job-preset/job-search-category/job-search-category.seed.ts deleted file mode 100644 index f3ba18bfcd8..00000000000 --- a/packages/core/src/employee-job-preset/job-search-category/job-search-category.seed.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DataSource } from 'typeorm'; -import { IOrganization, ITenant, JobPostSourceEnum } from '@gauzy/contracts'; -import { JobSearchCategory } from './job-search-category.entity'; - -export const createDefaultJobSearchCategories = async ( - dataSource: DataSource, - tenant: ITenant, - organization: IOrganization -): Promise => { - const categories: JobSearchCategory[] = []; - const upworkCategories = [ - { - name: 'IT & Networking', - jobSourceCategoryId: '531770282580668419' - }, - { - name: 'Web, Mobile & Software Dev', - jobSourceCategoryId: '531770282580668418' - } - ]; - - upworkCategories.forEach((category) => { - const cat = new JobSearchCategory(); - - cat.jobSource = JobPostSourceEnum.UPWORK; - cat.organizationId = organization.id; - cat.tenantId = tenant.id; - cat.name = category.name; - cat.jobSourceCategoryId = category.jobSourceCategoryId; - - categories.push(cat); - }); - - await insertCategories(dataSource, categories); - return categories; -}; - -const insertCategories = async ( - dataSource: DataSource, - categories: JobSearchCategory[] -): Promise => { - await dataSource.manager.save(categories); -}; diff --git a/packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.seed.ts b/packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.seed.ts deleted file mode 100644 index 4afad758520..00000000000 --- a/packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.seed.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { DataSource } from 'typeorm'; -import { IOrganization, ITenant, JobPostSourceEnum } from '@gauzy/contracts'; -import { JobSearchOccupation } from './job-search-occupation.entity'; - -export const createDefaultJobSearchOccupations = async ( - dataSource: DataSource, - tenant: ITenant, - organization: IOrganization -): Promise => { - const occupations: JobSearchOccupation[] = []; - - const upworkOccupations = [ - { - name: 'DevOps Engineering', - jobSourceOccupationId: '1110580753140797440' - }, - { - name: 'Project Management', - jobSourceOccupationId: '1017484851352698979' - } - ]; - - upworkOccupations.forEach((occupation) => { - const occ = new JobSearchOccupation(); - - occ.jobSource = JobPostSourceEnum.UPWORK; - occ.organizationId = organization.id; - occ.tenantId = tenant.id; - occ.name = occupation.name; - occ.jobSourceOccupationId = occupation.jobSourceOccupationId; - - occupations.push(occ); - }); - - await insertOccupations(dataSource, occupations); - return occupations; -}; - -const insertOccupations = async ( - dataSource: DataSource, - occupations: JobSearchOccupation[] -): Promise => { - await dataSource.manager.save(occupations); -}; diff --git a/packages/core/src/employee-job/employee-job.module.ts b/packages/core/src/employee-job/employee-job.module.ts deleted file mode 100644 index 5f1186bf02f..00000000000 --- a/packages/core/src/employee-job/employee-job.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module } from '@nestjs/common'; -import { RouterModule } from '@nestjs/core'; -import { GauzyAIModule } from '@gauzy/integration-ai'; -import { EmployeeJobPostService } from './employee-job.service'; -import { EmployeeJobPostController } from './employee-job.controller'; -import { EmployeeModule } from './../employee/employee.module'; -import { CountryModule } from './../country/country.module'; -import { IntegrationTenantModule } from './../integration-tenant/integration-tenant.module'; - -@Module({ - imports: [ - RouterModule.register([ - { path: '/employee-job', module: EmployeeJobPostModule } - ]), - CountryModule, - EmployeeModule, - IntegrationTenantModule, - GauzyAIModule.forRoot() - ], - controllers: [EmployeeJobPostController], - providers: [EmployeeJobPostService], - exports: [EmployeeJobPostService] -}) -export class EmployeeJobPostModule { } diff --git a/packages/core/src/employee-job/index.ts b/packages/core/src/employee-job/index.ts deleted file mode 100644 index b27ce71f533..00000000000 --- a/packages/core/src/employee-job/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { EmployeeJobPostModule } from './employee-job.module'; -export { EmployeeJobPost } from './employee-job.entity'; -export { EmployeeJobPostService } from './employee-job.service'; diff --git a/packages/core/src/employee/dto/employee-feature.dto.ts b/packages/core/src/employee/dto/employee-feature.dto.ts index 617afdeec3c..da940ba67d6 100644 --- a/packages/core/src/employee/dto/employee-feature.dto.ts +++ b/packages/core/src/employee/dto/employee-feature.dto.ts @@ -1,6 +1,6 @@ -import { IEmployee, IRelationalEmployee } from "@gauzy/contracts"; import { ApiPropertyOptional } from "@nestjs/swagger"; import { IsObject, IsString, ValidateIf } from "class-validator"; +import { IEmployee, IRelationalEmployee } from "@gauzy/contracts"; import { Employee } from "./../employee.entity"; import { IsEmployeeBelongsToOrganization } from "./../../shared/validators"; diff --git a/packages/core/src/employee/employee.entity.ts b/packages/core/src/employee/employee.entity.ts index 33536f6313b..4c2f2cf5428 100644 --- a/packages/core/src/employee/employee.entity.ts +++ b/packages/core/src/employee/employee.entity.ts @@ -2,21 +2,10 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { JoinColumn, JoinTable, RelationId } from 'typeorm'; import { EntityRepositoryType } from '@mikro-orm/core'; import { IsOptional, IsString } from 'class-validator'; -import { - ColumnIndex, - MultiORMColumn, - MultiORMEntity, - MultiORMManyToMany, - MultiORMManyToOne, - MultiORMOneToMany, - MultiORMOneToOne, - VirtualMultiOrmColumn -} from './../core/decorators/entity'; import { CurrenciesEnum, IEmployee, PayPeriodEnum, - ITag, IContact, ISkill, IUser, @@ -42,6 +31,17 @@ import { IEquipmentSharing, IEmployeePhone } from '@gauzy/contracts'; +import { + ColumnIndex, + EmbeddedColumn, + MultiORMColumn, + MultiORMEntity, + MultiORMManyToMany, + MultiORMManyToOne, + MultiORMOneToMany, + MultiORMOneToOne, + VirtualMultiOrmColumn +} from '../core/decorators/entity'; import { Candidate, Contact, @@ -52,7 +52,6 @@ import { Expense, Goal, InvoiceItem, - JobPreset, OrganizationContact, OrganizationDepartment, OrganizationEmploymentType, @@ -72,11 +71,13 @@ import { TimeSlot, User } from '../core/entities/internal'; -import { ColumnNumericTransformerPipe } from './../shared/pipes'; +import { CustomEmployeeFields, HasCustomFields } from '../core/entities/custom-entity-fields'; +import { ColumnNumericTransformerPipe } from '../shared/pipes'; +import { Taggable } from '../tags/tag.types'; import { MikroOrmEmployeeRepository } from './repository/mikro-orm-employee.repository'; @MultiORMEntity('employee', { mikroOrmRepository: () => MikroOrmEmployeeRepository }) -export class Employee extends TenantOrganizationBaseEntity implements IEmployee { +export class Employee extends TenantOrganizationBaseEntity implements IEmployee, HasCustomFields, Taggable { [EntityRepositoryType]?: MikroOrmEmployeeRepository; @ApiPropertyOptional({ type: () => Date }) @@ -545,7 +546,7 @@ export class Employee extends TenantOrganizationBaseEntity implements IEmployee @JoinTable({ name: 'tag_employee' }) - tags: ITag[]; + tags: Tag[]; /** * Employee Skills @@ -577,13 +578,6 @@ export class Employee extends TenantOrganizationBaseEntity implements IEmployee }) organizationEmploymentTypes?: IOrganizationEmploymentType[]; - /** - * Employee Job Presets - */ - @ApiProperty({ type: () => JobPreset }) - @MultiORMManyToMany(() => JobPreset, (jobPreset) => jobPreset.employees) - jobPresets?: JobPreset[]; - /** * Employee Organization Contacts */ @@ -643,4 +637,12 @@ export class Employee extends TenantOrganizationBaseEntity implements IEmployee onDelete: 'CASCADE' }) equipmentSharings?: IEquipmentSharing[]; + + /* + |-------------------------------------------------------------------------- + | Embeddable Columns + |-------------------------------------------------------------------------- + */ + @EmbeddedColumn(() => CustomEmployeeFields, { prefix: false }) + customFields?: CustomEmployeeFields; } diff --git a/packages/core/src/employee/employee.service.ts b/packages/core/src/employee/employee.service.ts index 038b97ea3ae..27580968542 100644 --- a/packages/core/src/employee/employee.service.ts +++ b/packages/core/src/employee/employee.service.ts @@ -3,6 +3,7 @@ import { Brackets, FindManyOptions, FindOneOptions, + In, SelectQueryBuilder, UpdateResult, WhereExpressionBuilder @@ -12,7 +13,6 @@ import { IBasePerTenantAndOrganizationEntityModel, IDateRangePicker, IEmployee, - IEmployeeFindInput, IOrganization, IPagination, PermissionsEnum @@ -34,6 +34,70 @@ export class EmployeeService extends TenantAwareCrudService { super(typeOrmEmployeeRepository, mikroOrmEmployeeRepository); } + /** + * Finds employees based on an array of user IDs. + * @param userIds An array of user IDs. + * @returns A promise resolving to an array of employees. + */ + async findEmployeesByUserIds(userIds: string[]): Promise { + try { + // Get the tenant ID from the current request context + const tenantId = RequestContext.currentTenantId(); + + // Construct the where clause based on whether tenantId is available + const whereClause = tenantId ? { tenantId, userId: In(userIds) } : { userId: In(userIds) }; + + // Execute the query based on the ORM type + switch (this.ormType) { + case MultiORMEnum.MikroORM: { + const { where, mikroOptions } = parseTypeORMFindToMikroOrm({ where: whereClause } as FindManyOptions); + const employees = await this.mikroOrmRepository.find(where, mikroOptions); + return employees.map((entity: Employee) => this.serialize(entity)) as Employee[]; + } + case MultiORMEnum.TypeORM: { + return await this.typeOrmRepository.find({ where: whereClause }); + } + default: + throw new Error(`Method not implemented for ORM type: ${this.ormType}`); + } + } catch (error) { + console.error(`Error finding employees by user IDs: ${error.message}`); + return []; // Return an empty array if an error occurs + } + } + + /** + * Finds the employeeId associated with a given userId. + * + * @param userId The ID of the user. + * @returns The employeeId or null if not found or in case of an error. + */ + async findEmployeeIdByUserId(userId: string): Promise { + try { + const tenantId = RequestContext.currentTenantId(); + // Construct the where clause based on whether tenantId is available + const whereClause = tenantId ? { tenantId, userId } : { userId }; + + switch (this.ormType) { + case MultiORMEnum.MikroORM: { + const employee = await this.mikroOrmRepository.findOne(whereClause); + return employee ? employee.id : null; + } + case MultiORMEnum.TypeORM: { + const employee = await this.typeOrmRepository.findOne({ where: whereClause }); + return employee ? employee.id : null; + } + default: + throw new Error(`Not implemented for ${this.ormType}`); + } + } catch (error) { + console.error(`Error finding employee by userId: ${error.message}`); + return null; + } + } + + + /** * Finds an employee by user ID. * diff --git a/packages/core/src/employee/index.ts b/packages/core/src/employee/index.ts index f2d733473ae..3eea723f02d 100644 --- a/packages/core/src/employee/index.ts +++ b/packages/core/src/employee/index.ts @@ -4,3 +4,5 @@ export * from './employee.entity'; export * from './employee.module'; export * from './employee.service'; export * from './employee.seed'; +export * from './dto/employee-feature.dto'; +export * from './repository'; diff --git a/packages/core/src/event-bus/event-bus.ts b/packages/core/src/event-bus/event-bus.ts index e96858545fc..721607429c8 100644 --- a/packages/core/src/event-bus/event-bus.ts +++ b/packages/core/src/event-bus/event-bus.ts @@ -1,6 +1,6 @@ import { Injectable, OnModuleDestroy } from "@nestjs/common"; import { Observable, Subject, filter, takeUntil } from "rxjs"; -import { isNotNullOrUndefined, ConstructorType } from '@gauzy/common'; +import { isNotNullOrUndefined, Type } from '@gauzy/common'; import { BaseEvent } from "./base-event"; @Injectable() @@ -24,7 +24,7 @@ export class EventBus implements OnModuleDestroy { * @param event The type of events to subscribe to. * @returns An Observable of events with the specified type. */ - ofType(event: ConstructorType): Observable { + ofType(event: Type): Observable { return this.event$.asObservable().pipe( takeUntil(this.onDestroy$), // Unsubscribe when the component is destroyed filter((item) => item.constructor === event), // diff --git a/packages/core/src/export-import/export/export.service.ts b/packages/core/src/export-import/export/export.service.ts index 2414bc6ac04..3c6f55a48da 100644 --- a/packages/core/src/export-import/export/export.service.ts +++ b/packages/core/src/export-import/export/export.service.ts @@ -1,10 +1,10 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; -import { InjectConnection, InjectRepository } from '@nestjs/typeorm'; -import { Connection, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { camelCase } from 'typeorm/util/StringUtils'; import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'; import { BehaviorSubject } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; -import * as _ from 'lodash'; import * as archiver from 'archiver'; import * as csv from 'csv-writer'; import * as fs from 'fs'; @@ -13,6 +13,7 @@ import * as fse from 'fs-extra'; import { ConfigService } from '@gauzy/config'; import { getEntitiesFromPlugins } from '@gauzy/plugin'; import { isFunction, isNotEmpty } from '@gauzy/common'; +import { ConnectionEntityManager } from '../../database/connection-entity-manager'; import { AccountingTemplate, Activity, @@ -42,10 +43,8 @@ import { EmployeeAppointment, EmployeeAward, EmployeeLevel, - EmployeeProposalTemplate, EmployeeRecurringExpense, EmployeeSetting, - EmployeeUpworkJobsSearchCriterion, Equipment, EquipmentSharing, EquipmentSharingPolicy, @@ -74,10 +73,6 @@ import { Invoice, InvoiceEstimateHistory, InvoiceItem, - JobPreset, - JobPresetUpworkJobSearchCriterion, - JobSearchCategory, - JobSearchOccupation, KeyResult, KeyResultTemplate, KeyResultUpdate, @@ -113,7 +108,6 @@ import { ProductVariant, ProductVariantPrice, ProductVariantSetting, - Proposal, Report, ReportCategory, ReportOrganization, @@ -191,20 +185,8 @@ import { MikroOrmEmployeeAppointmentRepository } from '../../employee-appointmen import { TypeOrmEmployeeAppointmentRepository } from '../../employee-appointment/repository/type-orm-employee-appointment.repository'; import { MikroOrmEmployeeAwardRepository } from '../../employee-award/repository/mikro-orm-employee-award.repository'; import { TypeOrmEmployeeAwardRepository } from '../../employee-award/repository/type-orm-employee-award.repository'; -import { MikroOrmJobSearchCategoryRepository } from '../../employee-job-preset/job-search-category/repository/mikro-orm-job-search-category.repository'; -import { TypeOrmJobSearchCategoryRepository } from '../../employee-job-preset/job-search-category/repository/type-orm-job-search-category.repository'; -import { MikroOrmJobSearchOccupationRepository } from '../../employee-job-preset/job-search-occupation/repository/mikro-orm-job-search-occupation.repository'; -import { TypeOrmJobSearchOccupationRepository } from '../../employee-job-preset/job-search-occupation/repository/type-orm-job-search-occupation.repository'; -import { MikroOrmEmployeeUpworkJobsSearchCriterionRepository } from '../../employee-job-preset/repository/mikro-orm-employee-upwork-jobs-search-criterion.entity.repository'; -import { MikroOrmJobPresetUpworkJobSearchCriterionRepository } from '../../employee-job-preset/repository/mikro-orm-job-preset-upwork-job-search-criterion.repository'; -import { MikroOrmJobPresetRepository } from '../../employee-job-preset/repository/mikro-orm-job-preset.repository'; -import { TypeOrmJobPresetUpworkJobSearchCriterionRepository } from '../../employee-job-preset/repository/type-orm-job-preset-upwork-job-search-criterion.repository'; -import { TypeOrmJobPresetRepository } from '../../employee-job-preset/repository/type-orm-job-preset.repository'; -import { TypeOrmEmployeeUpworkJobsSearchCriterionRepository } from '../../employee-job-preset/repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository'; import { MikroOrmEmployeeLevelRepository } from '../../employee-level/repository/mikro-orm-employee-level.repository'; import { TypeOrmEmployeeLevelRepository } from '../../employee-level/repository/type-orm-employee-level.repository'; -import { MikroOrmEmployeeProposalTemplateRepository } from '../../employee-proposal-template/repository/mikro-orm-employee-proposal-template.repository'; -import { TypeOrmEmployeeProposalTemplateRepository } from '../../employee-proposal-template/repository/type-orm-employee-proposal-template.repository'; import { MikroOrmEmployeeRecurringExpenseRepository } from '../../employee-recurring-expense/repository/mikro-orm-employee-recurring-expense.repository'; import { TypeOrmEmployeeRecurringExpenseRepository } from '../../employee-recurring-expense/repository/type-orm-employee-recurring-expense.repository'; import { MikroOrmEmployeeSettingRepository } from '../../employee-setting/repository/mikro-orm-employee-setting.repository'; @@ -337,8 +319,6 @@ import { MikroOrmProductTranslationRepository } from '../../product/repository/m import { MikroOrmProductRepository } from '../../product/repository/mikro-orm-product.repository'; import { TypeOrmProductTranslationRepository } from '../../product/repository/type-orm-product-translation.repository'; import { TypeOrmProductRepository } from '../../product/repository/type-orm-product.repository'; -import { MikroOrmProposalRepository } from '../../proposal/repository/mikro-orm-proposal.repository'; -import { TypeOrmProposalRepository } from '../../proposal/repository/type-orm-proposal.repository'; import { MikroOrmReportCategoryRepository } from '../../reports/repository/mikro-orm-report-category.repository'; import { MikroOrmReportOrganizationRepository } from '../../reports/repository/mikro-orm-report-organization.repository'; import { MikroOrmReportRepository } from '../../reports/repository/mikro-orm-report.repository'; @@ -550,11 +530,6 @@ export class ExportService implements OnModuleInit { mikroOrmEmployeeAwardRepository: MikroOrmEmployeeAwardRepository, - @InjectRepository(EmployeeProposalTemplate) - private typeOrmEmployeeProposalTemplateRepository: TypeOrmEmployeeProposalTemplateRepository, - - mikroOrmEmployeeProposalTemplateRepository: MikroOrmEmployeeProposalTemplateRepository, - @InjectRepository(EmployeeRecurringExpense) private typeOrmEmployeeRecurringExpenseRepository: TypeOrmEmployeeRecurringExpenseRepository, @@ -565,11 +540,6 @@ export class ExportService implements OnModuleInit { mikroOrmEmployeeSettingRepository: MikroOrmEmployeeSettingRepository, - @InjectRepository(EmployeeUpworkJobsSearchCriterion) - private typeOrmEmployeeUpworkJobsSearchCriterionRepository: TypeOrmEmployeeUpworkJobsSearchCriterionRepository, - - mikroOrmEmployeeUpworkJobsSearchCriterionRepository: MikroOrmEmployeeUpworkJobsSearchCriterionRepository, - @InjectRepository(Equipment) private typeOrmEquipmentRepository: TypeOrmEquipmentRepository, @@ -705,26 +675,6 @@ export class ExportService implements OnModuleInit { mikroOrmInvoiceItemRepository: MikroOrmInvoiceItemRepository, - @InjectRepository(JobPreset) - private typeOrmJobPresetRepository: TypeOrmJobPresetRepository, - - mikroOrmJobPresetRepository: MikroOrmJobPresetRepository, - - @InjectRepository(JobPresetUpworkJobSearchCriterion) - private typeOrmJobPresetUpworkJobSearchCriterionRepository: TypeOrmJobPresetUpworkJobSearchCriterionRepository, - - mikroOrmJobPresetUpworkJobSearchCriterionRepository: MikroOrmJobPresetUpworkJobSearchCriterionRepository, - - @InjectRepository(JobSearchCategory) - private typeOrmJobSearchCategoryRepository: TypeOrmJobSearchCategoryRepository, - - mikroOrmJobSearchCategoryRepository: MikroOrmJobSearchCategoryRepository, - - @InjectRepository(JobSearchOccupation) - private typeOrmJobSearchOccupationRepository: TypeOrmJobSearchOccupationRepository, - - mikroOrmJobSearchOccupationRepository: MikroOrmJobSearchOccupationRepository, - @InjectRepository(KeyResult) private typeOrmKeyResultRepository: TypeOrmKeyResultRepository, @@ -925,11 +875,6 @@ export class ExportService implements OnModuleInit { mikroOrmWarehouseProductVariantRepository: MikroOrmWarehouseProductVariantRepository, - @InjectRepository(Proposal) - private typeOrmProposalRepository: TypeOrmProposalRepository, - - mikroOrmProposalRepository: MikroOrmProposalRepository, - @InjectRepository(Skill) private typeOrmSkillRepository: TypeOrmSkillRepository, @@ -1040,10 +985,8 @@ export class ExportService implements OnModuleInit { mikroOrmUserOrganizationRepository: MikroOrmUserOrganizationRepository, - @InjectConnection() - private readonly dataSource: Connection, - - private readonly configService: ConfigService + private readonly configService: ConfigService, + private readonly _connectionEntityManager: ConnectionEntityManager ) { } async onModuleInit() { @@ -1402,8 +1345,8 @@ export class ExportService implements OnModuleInit { continue; } - const className = _.camelCase(entity.name); - const repository = this.dataSource.getRepository(entity); + const className = camelCase(entity.name); + const repository = this._connectionEntityManager.getRepository(entity); this[className] = repository; this.dynamicEntitiesClassMap.push({ repository }); @@ -1506,25 +1449,18 @@ export class ExportService implements OnModuleInit { { joinTableName: 'tag_organization_employee_level' } ] }, - { - repository: this.typeOrmEmployeeProposalTemplateRepository - }, { repository: this.typeOrmEmployeeRecurringExpenseRepository }, { repository: this.typeOrmEmployeeRepository, relations: [ - { joinTableName: 'employee_job_preset' }, { joinTableName: 'tag_employee' } ] }, { repository: this.typeOrmEmployeeSettingRepository }, - { - repository: this.typeOrmEmployeeUpworkJobsSearchCriterionRepository - }, { repository: this.typeOrmEquipmentRepository, relations: [ @@ -1640,18 +1576,6 @@ export class ExportService implements OnModuleInit { { joinTableName: 'tag_invoice' } ] }, - { - repository: this.typeOrmJobPresetRepository - }, - { - repository: this.typeOrmJobPresetUpworkJobSearchCriterionRepository - }, - { - repository: this.typeOrmJobSearchCategoryRepository - }, - { - repository: this.typeOrmJobSearchOccupationRepository - }, { repository: this.typeOrmKeyResultRepository }, @@ -1812,12 +1736,6 @@ export class ExportService implements OnModuleInit { { repository: this.typeOrmWarehouseProductVariantRepository }, - { - repository: this.typeOrmProposalRepository, - relations: [ - { joinTableName: 'tag_proposal' } - ] - }, { repository: this.typeOrmReportCategoryRepository, tenantBase: false @@ -1873,10 +1791,7 @@ export class ExportService implements OnModuleInit { }, { repository: this.typeOrmTenantRepository, - condition: { - column: 'id', - replace: 'tenantId' - } + condition: { column: 'id', replace: 'tenantId' } }, { repository: this.typeOrmTenantSettingRepository diff --git a/packages/core/src/export-import/import/import.service.ts b/packages/core/src/export-import/import/import.service.ts index 8b38146a24b..86d5d4d2b1e 100644 --- a/packages/core/src/export-import/import/import.service.ts +++ b/packages/core/src/export-import/import/import.service.ts @@ -1,18 +1,19 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; -import { InjectConnection, InjectRepository } from '@nestjs/typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; import { CommandBus } from '@nestjs/cqrs'; -import { Connection, IsNull, Repository } from 'typeorm'; +import { IsNull, Repository } from 'typeorm'; +import { camelCase } from 'typeorm/util/StringUtils'; import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'; import * as fs from 'fs'; import * as unzipper from 'unzipper'; import * as csv from 'csv-parser'; import * as rimraf from 'rimraf'; -import * as _ from 'lodash'; import * as path from 'path'; import * as chalk from 'chalk'; import { ConfigService } from '@gauzy/config'; import { getEntitiesFromPlugins } from '@gauzy/plugin'; import { isFunction, isNotEmpty } from '@gauzy/common'; +import { ConnectionEntityManager } from '../../database/connection-entity-manager'; import { convertToDatetime } from '../../core/utils'; import { FileStorage } from '../../core/file-storage'; import { @@ -42,10 +43,8 @@ import { EmployeeAppointment, EmployeeAward, EmployeeLevel, - EmployeeProposalTemplate, EmployeeRecurringExpense, EmployeeSetting, - EmployeeUpworkJobsSearchCriterion, Equipment, EquipmentSharing, EquipmentSharingPolicy, @@ -74,10 +73,6 @@ import { Invoice, InvoiceEstimateHistory, InvoiceItem, - JobPreset, - JobPresetUpworkJobSearchCriterion, - JobSearchCategory, - JobSearchOccupation, KeyResult, KeyResultTemplate, KeyResultUpdate, @@ -113,7 +108,6 @@ import { ProductVariant, ProductVariantPrice, ProductVariantSetting, - Proposal, Report, ReportCategory, ReportOrganization, @@ -188,20 +182,8 @@ import { MikroOrmEmployeeAppointmentRepository } from '../../employee-appointmen import { TypeOrmEmployeeAppointmentRepository } from '../../employee-appointment/repository/type-orm-employee-appointment.repository'; import { MikroOrmEmployeeAwardRepository } from '../../employee-award/repository/mikro-orm-employee-award.repository'; import { TypeOrmEmployeeAwardRepository } from '../../employee-award/repository/type-orm-employee-award.repository'; -import { MikroOrmJobSearchCategoryRepository } from '../../employee-job-preset/job-search-category/repository/mikro-orm-job-search-category.repository'; -import { TypeOrmJobSearchCategoryRepository } from '../../employee-job-preset/job-search-category/repository/type-orm-job-search-category.repository'; -import { MikroOrmJobSearchOccupationRepository } from '../../employee-job-preset/job-search-occupation/repository/mikro-orm-job-search-occupation.repository'; -import { TypeOrmJobSearchOccupationRepository } from '../../employee-job-preset/job-search-occupation/repository/type-orm-job-search-occupation.repository'; -import { MikroOrmEmployeeUpworkJobsSearchCriterionRepository } from '../../employee-job-preset/repository/mikro-orm-employee-upwork-jobs-search-criterion.entity.repository'; -import { MikroOrmJobPresetUpworkJobSearchCriterionRepository } from '../../employee-job-preset/repository/mikro-orm-job-preset-upwork-job-search-criterion.repository'; -import { MikroOrmJobPresetRepository } from '../../employee-job-preset/repository/mikro-orm-job-preset.repository'; -import { TypeOrmJobPresetUpworkJobSearchCriterionRepository } from '../../employee-job-preset/repository/type-orm-job-preset-upwork-job-search-criterion.repository'; -import { TypeOrmJobPresetRepository } from '../../employee-job-preset/repository/type-orm-job-preset.repository'; -import { TypeOrmEmployeeUpworkJobsSearchCriterionRepository } from '../../employee-job-preset/repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository'; import { MikroOrmEmployeeLevelRepository } from '../../employee-level/repository/mikro-orm-employee-level.repository'; import { TypeOrmEmployeeLevelRepository } from '../../employee-level/repository/type-orm-employee-level.repository'; -import { MikroOrmEmployeeProposalTemplateRepository } from '../../employee-proposal-template/repository/mikro-orm-employee-proposal-template.repository'; -import { TypeOrmEmployeeProposalTemplateRepository } from '../../employee-proposal-template/repository/type-orm-employee-proposal-template.repository'; import { MikroOrmEmployeeRecurringExpenseRepository } from '../../employee-recurring-expense/repository/mikro-orm-employee-recurring-expense.repository'; import { TypeOrmEmployeeRecurringExpenseRepository } from '../../employee-recurring-expense/repository/type-orm-employee-recurring-expense.repository'; import { MikroOrmEmployeeSettingRepository } from '../../employee-setting/repository/mikro-orm-employee-setting.repository'; @@ -334,8 +316,6 @@ import { MikroOrmProductTranslationRepository } from '../../product/repository/m import { MikroOrmProductRepository } from '../../product/repository/mikro-orm-product.repository'; import { TypeOrmProductTranslationRepository } from '../../product/repository/type-orm-product-translation.repository'; import { TypeOrmProductRepository } from '../../product/repository/type-orm-product.repository'; -import { MikroOrmProposalRepository } from '../../proposal/repository/mikro-orm-proposal.repository'; -import { TypeOrmProposalRepository } from '../../proposal/repository/type-orm-proposal.repository'; import { MikroOrmReportCategoryRepository } from '../../reports/repository/mikro-orm-report-category.repository'; import { MikroOrmReportOrganizationRepository } from '../../reports/repository/mikro-orm-report-organization.repository'; import { MikroOrmReportRepository } from '../../reports/repository/mikro-orm-report.repository'; @@ -544,11 +524,6 @@ export class ImportService implements OnModuleInit { mikroOrmEmployeeAwardRepository: MikroOrmEmployeeAwardRepository, - @InjectRepository(EmployeeProposalTemplate) - private typeOrmEmployeeProposalTemplateRepository: TypeOrmEmployeeProposalTemplateRepository, - - mikroOrmEmployeeProposalTemplateRepository: MikroOrmEmployeeProposalTemplateRepository, - @InjectRepository(EmployeeRecurringExpense) private typeOrmEmployeeRecurringExpenseRepository: TypeOrmEmployeeRecurringExpenseRepository, @@ -559,11 +534,6 @@ export class ImportService implements OnModuleInit { mikroOrmEmployeeSettingRepository: MikroOrmEmployeeSettingRepository, - @InjectRepository(EmployeeUpworkJobsSearchCriterion) - private typeOrmEmployeeUpworkJobsSearchCriterionRepository: TypeOrmEmployeeUpworkJobsSearchCriterionRepository, - - mikroOrmEmployeeUpworkJobsSearchCriterionRepository: MikroOrmEmployeeUpworkJobsSearchCriterionRepository, - @InjectRepository(Equipment) private typeOrmEquipmentRepository: TypeOrmEquipmentRepository, @@ -699,26 +669,6 @@ export class ImportService implements OnModuleInit { mikroOrmInvoiceItemRepository: MikroOrmInvoiceItemRepository, - @InjectRepository(JobPreset) - private typeOrmJobPresetRepository: TypeOrmJobPresetRepository, - - mikroOrmJobPresetRepository: MikroOrmJobPresetRepository, - - @InjectRepository(JobPresetUpworkJobSearchCriterion) - private typeOrmJobPresetUpworkJobSearchCriterionRepository: TypeOrmJobPresetUpworkJobSearchCriterionRepository, - - mikroOrmJobPresetUpworkJobSearchCriterionRepository: MikroOrmJobPresetUpworkJobSearchCriterionRepository, - - @InjectRepository(JobSearchCategory) - private typeOrmJobSearchCategoryRepository: TypeOrmJobSearchCategoryRepository, - - mikroOrmJobSearchCategoryRepository: MikroOrmJobSearchCategoryRepository, - - @InjectRepository(JobSearchOccupation) - private typeOrmJobSearchOccupationRepository: TypeOrmJobSearchOccupationRepository, - - mikroOrmJobSearchOccupationRepository: MikroOrmJobSearchOccupationRepository, - @InjectRepository(KeyResult) private typeOrmKeyResultRepository: TypeOrmKeyResultRepository, @@ -919,11 +869,6 @@ export class ImportService implements OnModuleInit { mikroOrmWarehouseProductVariantRepository: MikroOrmWarehouseProductVariantRepository, - @InjectRepository(Proposal) - private typeOrmProposalRepository: TypeOrmProposalRepository, - - mikroOrmProposalRepository: MikroOrmProposalRepository, - @InjectRepository(Skill) private typeOrmSkillRepository: TypeOrmSkillRepository, @@ -1029,10 +974,8 @@ export class ImportService implements OnModuleInit { mikroOrmUserOrganizationRepository: MikroOrmUserOrganizationRepository, - @InjectConnection() - private dataSource: Connection, - private readonly configService: ConfigService, + private readonly _connectionEntityManager: ConnectionEntityManager, private readonly commandBus: CommandBus ) { } @@ -1365,13 +1308,11 @@ export class ImportService implements OnModuleInit { continue; } - const className = _.camelCase(entity.name); - const repository = this.dataSource.getRepository(entity); + const className = camelCase(entity.name); + const repository = this._connectionEntityManager.getRepository(entity); this[className] = repository; - this.dynamicEntitiesClassMap.push({ - repository - }); + this.dynamicEntitiesClassMap.push({ repository }); } } @@ -1519,24 +1460,6 @@ export class ImportService implements OnModuleInit { { column: 'reportId', repository: this.typeOrmReportRepository } ] }, - { - repository: this.typeOrmJobPresetRepository - }, - { - repository: this.typeOrmJobSearchCategoryRepository - }, - { - repository: this.typeOrmJobSearchOccupationRepository - }, - { - repository: this.typeOrmJobPresetUpworkJobSearchCriterionRepository, - isCheckRelation: true, - foreignKeys: [ - { column: 'jobPresetId', repository: this.typeOrmJobPresetRepository }, - { column: 'occupationId', repository: this.typeOrmJobSearchOccupationRepository }, - { column: 'categoryId', repository: this.typeOrmJobSearchCategoryRepository } - ] - }, /** * These entities need TENANT, ORGANIZATION & USER */ @@ -1767,13 +1690,6 @@ export class ImportService implements OnModuleInit { { column: 'employeeId', repository: this.typeOrmEmployeeRepository } ] }, - { - repository: this.typeOrmEmployeeProposalTemplateRepository, - isCheckRelation: true, - foreignKeys: [ - { column: 'employeeId', repository: this.typeOrmEmployeeRepository } - ] - }, { repository: this.typeOrmEmployeeRecurringExpenseRepository, isCheckRelation: true, @@ -1788,16 +1704,6 @@ export class ImportService implements OnModuleInit { { column: 'employeeId', repository: this.typeOrmEmployeeRepository } ] }, - { - repository: this.typeOrmEmployeeUpworkJobsSearchCriterionRepository, - isCheckRelation: true, - foreignKeys: [ - { column: 'employeeId', repository: this.typeOrmEmployeeRepository }, - { column: 'jobPresetId', repository: this.typeOrmJobPresetRepository }, - { column: 'occupationId', repository: this.typeOrmJobSearchOccupationRepository }, - { column: 'categoryId', repository: this.typeOrmJobSearchCategoryRepository } - ] - }, { repository: this.typeOrmEmployeeLevelRepository }, @@ -2195,17 +2101,6 @@ export class ImportService implements OnModuleInit { ], }, /* - * Proposal & Related Entities - */ - { - repository: this.typeOrmProposalRepository, - isCheckRelation: true, - foreignKeys: [ - { column: 'employeeId', repository: this.typeOrmEmployeeRepository }, - { column: 'organizationContactId', repository: this.typeOrmOrganizationContactRepository } - ] - }, - /* * Payment & Related Entities */ { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 93491447510..f7b24835c65 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -6,6 +6,8 @@ export * from './tenant'; export * from './role-permission'; export * from './user'; export * from './organization'; +export * from './integration-tenant' export * from './core/seeds'; export * from './employee'; -export * from './database/migration-executor'; +export * from './tags'; +export * from './database'; diff --git a/packages/core/src/integration-tenant/index.ts b/packages/core/src/integration-tenant/index.ts new file mode 100644 index 00000000000..23cd73e37fb --- /dev/null +++ b/packages/core/src/integration-tenant/index.ts @@ -0,0 +1,2 @@ +export * from './integration-tenant.module'; +export * from './integration-tenant.service'; diff --git a/packages/core/src/integration-tenant/integration-tenant.module.ts b/packages/core/src/integration-tenant/integration-tenant.module.ts index cbdd0955b2d..8ed8ceef953 100644 --- a/packages/core/src/integration-tenant/integration-tenant.module.ts +++ b/packages/core/src/integration-tenant/integration-tenant.module.ts @@ -11,10 +11,13 @@ import { IntegrationEntitySettingModule } from './../integration-entity-setting/ import { RoleModule } from '../role/role.module'; import { CommandHandlers } from './commands/handlers'; import { RolePermissionModule } from '../role-permission/role-permission.module'; +import { TypeOrmIntegrationTenantRepository } from './repository'; @Module({ imports: [ - RouterModule.register([{ path: '/integration-tenant', module: IntegrationTenantModule }]), + RouterModule.register([ + { path: '/integration-tenant', module: IntegrationTenantModule } + ]), TypeOrmModule.forFeature([IntegrationTenant]), MikroOrmModule.forFeature([IntegrationTenant]), RoleModule, @@ -23,8 +26,8 @@ import { RolePermissionModule } from '../role-permission/role-permission.module' forwardRef(() => IntegrationEntitySettingModule), CqrsModule ], - exports: [IntegrationTenantService], controllers: [IntegrationTenantController], - providers: [IntegrationTenantService, ...CommandHandlers] + providers: [IntegrationTenantService, TypeOrmIntegrationTenantRepository, ...CommandHandlers], + exports: [TypeOrmModule, MikroOrmModule, IntegrationTenantService, TypeOrmIntegrationTenantRepository], }) export class IntegrationTenantModule { } diff --git a/packages/core/src/integration-tenant/integration-tenant.service.ts b/packages/core/src/integration-tenant/integration-tenant.service.ts index ea81abcf09f..cd4c8de8df0 100644 --- a/packages/core/src/integration-tenant/integration-tenant.service.ts +++ b/packages/core/src/integration-tenant/integration-tenant.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; import { FindManyOptions, IsNull, Not } from 'typeorm'; import { IIntegrationEntitySetting, @@ -10,8 +9,8 @@ import { IPagination, IntegrationEntity } from '@gauzy/contracts'; -import { RequestContext } from 'core/context'; -import { TenantAwareCrudService } from 'core/crud'; +import { RequestContext } from '../core/context'; +import { TenantAwareCrudService } from '../core/crud'; import { IntegrationTenant } from './integration-tenant.entity'; import { MikroOrmIntegrationTenantRepository } from './repository/mikro-orm-integration-tenant.repository'; import { TypeOrmIntegrationTenantRepository } from './repository/type-orm-integration-tenant.repository'; @@ -19,9 +18,7 @@ import { TypeOrmIntegrationTenantRepository } from './repository/type-orm-integr @Injectable() export class IntegrationTenantService extends TenantAwareCrudService { constructor( - @InjectRepository(IntegrationTenant) typeOrmIntegrationTenantRepository: TypeOrmIntegrationTenantRepository, - mikroOrmIntegrationTenantRepository: MikroOrmIntegrationTenantRepository ) { super(typeOrmIntegrationTenantRepository, mikroOrmIntegrationTenantRepository); diff --git a/packages/core/src/integration-tenant/repository/index.ts b/packages/core/src/integration-tenant/repository/index.ts new file mode 100644 index 00000000000..274b93922f0 --- /dev/null +++ b/packages/core/src/integration-tenant/repository/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-integration-tenant.repository'; +export * from './type-orm-integration-tenant.repository'; diff --git a/packages/core/src/invite/invite.seed.ts b/packages/core/src/invite/invite.seed.ts index 7e1cf8e3551..39e22a0d7f6 100644 --- a/packages/core/src/invite/invite.seed.ts +++ b/packages/core/src/invite/invite.seed.ts @@ -5,7 +5,8 @@ import { sign } from 'jsonwebtoken'; import { environment as env } from '@gauzy/config'; import * as moment from 'moment'; import { Invite } from './invite.entity'; -import { Role } from './../core/entities/internal'; +import { Role } from '../core/entities/internal'; +import { getEmailWithPostfix } from '../core/seeds/utils'; export const createDefaultEmployeeInviteSent = async ( dataSource: DataSource, @@ -22,7 +23,7 @@ export const createDefaultEmployeeInviteSent = async ( organizations.forEach((organization: IOrganization) => { for (let i = 0; i < 10; i++) { const invitee = new Invite(); - invitee.email = faker.internet.exampleEmail(); + invitee.email = getEmailWithPostfix(faker.internet.exampleEmail()); invitee.expireDate = faker.date.between({ from: new Date(), to: moment(new Date()).add(30, 'days').toDate() @@ -59,7 +60,7 @@ export const createRandomEmployeeInviteSent = async ( organizations.forEach((organization: IOrganization) => { for (let i = 0; i < noOfInvitesPerOrganization; i++) { const invitee = new Invite(); - invitee.email = faker.internet.exampleEmail(); + invitee.email = getEmailWithPostfix(faker.internet.exampleEmail()); invitee.expireDate = faker.date.between({ from: new Date(), to: moment(new Date()).add(30, 'days').toDate() diff --git a/packages/core/src/merchant/merchant.seed.ts b/packages/core/src/merchant/merchant.seed.ts index a69817a55ce..dabff3bcf07 100644 --- a/packages/core/src/merchant/merchant.seed.ts +++ b/packages/core/src/merchant/merchant.seed.ts @@ -1,7 +1,8 @@ import { DataSource } from 'typeorm'; -import { Merchant, Contact, ImageAsset, Country } from './../core/entities/internal'; import { faker } from '@faker-js/faker'; import { ICountry, IMerchant, IOrganization, ITenant } from '@gauzy/contracts'; +import { Merchant, Contact, ImageAsset, Country } from './../core/entities/internal'; +import { getEmailWithPostfix } from '../core/seeds/utils'; export const createRandomMerchants = async ( dataSource: DataSource, @@ -56,7 +57,7 @@ const applyRandomProperties = ( const merchant = new Merchant() merchant.name = faker.company.name(); merchant.code = faker.string.alphanumeric(); - merchant.email = faker.internet.exampleEmail(merchant.name); + merchant.email = getEmailWithPostfix(faker.internet.exampleEmail(merchant.name)); merchant.description = faker.lorem.words(); merchant.phone = faker.phone.number(); merchant.organization = organization; diff --git a/packages/core/src/organization-contact/organization-contact.entity.ts b/packages/core/src/organization-contact/organization-contact.entity.ts index 9b08b7a1852..a32ab0bb181 100644 --- a/packages/core/src/organization-contact/organization-contact.entity.ts +++ b/packages/core/src/organization-contact/organization-contact.entity.ts @@ -30,7 +30,6 @@ import { Invoice, OrganizationProject, Payment, - Proposal, Tag, TenantOrganizationBaseEntity, TimeLog @@ -181,12 +180,6 @@ export class OrganizationContact extends TenantOrganizationBaseEntity implements @JoinColumn() payments?: IPayment[]; - // Organization Proposals - @ApiPropertyOptional({ type: () => Proposal, isArray: true }) - @MultiORMOneToMany(() => Proposal, (it) => it.organizationContact) - @JoinColumn() - proposals?: IOrganizationProject[]; - /** * Expense */ diff --git a/packages/core/src/organization-contact/organization-contact.seed.ts b/packages/core/src/organization-contact/organization-contact.seed.ts index abf50a7d22f..7f0f3534845 100644 --- a/packages/core/src/organization-contact/organization-contact.seed.ts +++ b/packages/core/src/organization-contact/organization-contact.seed.ts @@ -11,9 +11,10 @@ import { OrganizationContactBudgetTypeEnum } from '@gauzy/contracts'; import * as _ from 'underscore'; -import { getDummyImage } from '../core'; +import { getDummyImage } from '../core/utils'; import { Organization, OrganizationContact, Tag } from './../core/entities/internal'; -import { getRandomContact } from 'contact/contact.seed'; +import { getEmailWithPostfix } from '../core/seeds/utils'; +import { getRandomContact } from '../contact/contact.seed'; export const createDefaultOrganizationContact = async ( dataSource: DataSource, @@ -103,7 +104,7 @@ const generateOrganizationContact = async ( ? faker.number.int({ min: 500, max: 5000 }) : faker.number.int({ min: 40, max: 400 }); - const email = faker.internet.exampleEmail(contact.firstName, contact.lastName); + const email = getEmailWithPostfix(faker.internet.exampleEmail(contact.firstName, contact.lastName)); orgContact.inviteStatus = faker.helpers.arrayElement(Object.values(ContactOrganizationInviteStatus)); const phone = faker.phone.number(); diff --git a/packages/core/src/organization/organization.subscriber.ts b/packages/core/src/organization/organization.subscriber.ts index b0d171a5373..835db70ce72 100644 --- a/packages/core/src/organization/organization.subscriber.ts +++ b/packages/core/src/organization/organization.subscriber.ts @@ -24,12 +24,11 @@ export class OrganizationSubscriber extends BaseEntityEventSubscriber { try { // Check if there's an existing image with a full URL - if (entity.image && entity.image.fullUrl) { + if (entity.image?.fullUrl) { entity.imageUrl = entity.image.fullUrl; } // If not, and if the imageUrl is not already set, generate a dummy image URL - else if (!entity.imageUrl) { - console.log('OrganizationSubscriber: generate dummy image for entity.name', entity.name); + else if (!entity.imageUrl && (entity.name || entity.officialName)) { entity.imageUrl = getOrganizationDummyImage(entity.name || entity.officialName); } } catch (error) { diff --git a/packages/core/src/pipeline/pipeline.module.ts b/packages/core/src/pipeline/pipeline.module.ts index ea6419c5c0a..7fd12d653ca 100644 --- a/packages/core/src/pipeline/pipeline.module.ts +++ b/packages/core/src/pipeline/pipeline.module.ts @@ -7,22 +7,24 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Pipeline } from './pipeline.entity'; import { StageModule } from '../pipeline-stage/pipeline-stage.module'; import { DealModule } from '../deal/deal.module'; -import { Deal } from '../deal/deal.entity'; import { RolePermissionModule } from '../role-permission/role-permission.module'; import { UserModule } from './../user/user.module'; +import { TypeOrmPipelineRepository } from './repository'; @Module({ imports: [ - RouterModule.register([{ path: '/pipelines', module: PipelineModule }]), - TypeOrmModule.forFeature([Pipeline, Deal]), - MikroOrmModule.forFeature([Pipeline, Deal]), + RouterModule.register([ + { path: '/pipelines', module: PipelineModule } + ]), + TypeOrmModule.forFeature([Pipeline]), + MikroOrmModule.forFeature([Pipeline]), StageModule, DealModule, RolePermissionModule, UserModule ], controllers: [PipelineController], - providers: [PipelineService], - exports: [PipelineService] + providers: [PipelineService, TypeOrmPipelineRepository], + exports: [PipelineService, TypeOrmPipelineRepository] }) export class PipelineModule { } diff --git a/packages/core/src/pipeline/pipeline.service.ts b/packages/core/src/pipeline/pipeline.service.ts index 94444d9fa98..26de7e67f62 100644 --- a/packages/core/src/pipeline/pipeline.service.ts +++ b/packages/core/src/pipeline/pipeline.service.ts @@ -1,42 +1,33 @@ import { Injectable } from '@nestjs/common'; -import { InjectConnection, InjectRepository } from '@nestjs/typeorm'; -import { Connection, DeepPartial, FindManyOptions, FindOptionsWhere, Raw, UpdateResult } from 'typeorm'; +import { DeepPartial, FindManyOptions, FindOptionsWhere, Raw, UpdateResult } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import { IPagination, IPipeline, IPipelineStage } from '@gauzy/contracts'; +import { IDeal, IPagination, IPipeline, IPipelineStage } from '@gauzy/contracts'; import { isPostgres } from '@gauzy/config'; +import { ConnectionEntityManager } from '../database/connection-entity-manager'; +import { prepareSQLQuery as p } from './../database/database.helper'; import { Pipeline } from './pipeline.entity'; -import { Deal, PipelineStage, User } from './../core/entities/internal'; +import { PipelineStage } from './../core/entities/internal'; import { RequestContext } from '../core/context'; import { TenantAwareCrudService } from './../core/crud'; -import { prepareSQLQuery as p } from './../database/database.helper'; -import { TypeOrmDealRepository } from '../deal/repository/type-orm-deal.repository'; -import { TypeOrmUserRepository } from '../user/repository/type-orm-user.repository'; -import { TypeOrmPipelineRepository } from './repository/type-orm-pipeline.repository'; -import { MikroOrmPipelineRepository } from './repository/mikro-orm-pipeline.repository'; +import { TypeOrmDealRepository } from '../deal/repository'; +import { TypeOrmUserRepository } from '../user/repository'; +import { MikroOrmPipelineRepository, TypeOrmPipelineRepository } from './repository'; @Injectable() export class PipelineService extends TenantAwareCrudService { public constructor( - @InjectRepository(Pipeline) - readonly typeOrmPipelineRepository: TypeOrmPipelineRepository, - - readonly mikroOrmPipelineRepository: MikroOrmPipelineRepository, - - @InjectRepository(Deal) - readonly typeOrmDealRepository: TypeOrmDealRepository, - - @InjectRepository(User) - readonly typeOrmUserRepository: TypeOrmUserRepository, - - @InjectConnection() - private readonly connection: Connection + private readonly typeOrmPipelineRepository: TypeOrmPipelineRepository, + private readonly mikroOrmPipelineRepository: MikroOrmPipelineRepository, + private readonly typeOrmDealRepository: TypeOrmDealRepository, + private readonly typeOrmUserRepository: TypeOrmUserRepository, + private readonly _connectionEntityManager: ConnectionEntityManager ) { super(typeOrmPipelineRepository, mikroOrmPipelineRepository); } public async findDeals(pipelineId: string) { const tenantId = RequestContext.currentTenantId(); - const items: Deal[] = await this.typeOrmDealRepository + const items: IDeal[] = await this.typeOrmDealRepository .createQueryBuilder('deal') .leftJoin('deal.stage', 'pipeline_stage') .where(p('pipeline_stage.pipelineId = :pipelineId'), { pipelineId }) @@ -69,7 +60,7 @@ export class PipelineService extends TenantAwareCrudService { id: string | number | FindOptionsWhere, entity: QueryDeepPartialEntity ): Promise { - const queryRunner = this.connection.createQueryRunner(); + const queryRunner = this._connectionEntityManager.rawConnection.createQueryRunner(); try { /** @@ -112,6 +103,7 @@ export class PipelineService extends TenantAwareCrudService { return saved; } catch (error) { + console.log('Rollback Pipeline Transaction', error); await queryRunner.rollbackTransaction(); } finally { await queryRunner.release(); diff --git a/packages/core/src/pipeline/repository/index.ts b/packages/core/src/pipeline/repository/index.ts new file mode 100644 index 00000000000..d0601ae5011 --- /dev/null +++ b/packages/core/src/pipeline/repository/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-pipeline.repository'; +export * from './type-orm-pipeline.repository'; diff --git a/packages/core/src/proposal/commands/handlers/index.ts b/packages/core/src/proposal/commands/handlers/index.ts deleted file mode 100644 index a093ddecfb0..00000000000 --- a/packages/core/src/proposal/commands/handlers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ProposalCreateHandler } from './proposal-create.handler'; - -export const CommandHandlers = [ProposalCreateHandler]; diff --git a/packages/core/src/proposal/proposal.controller.ts b/packages/core/src/proposal/proposal.controller.ts deleted file mode 100644 index ebede06cb6a..00000000000 --- a/packages/core/src/proposal/proposal.controller.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { - Controller, - HttpStatus, - Post, - Body, - Get, - Query, - UseGuards, - Put, - Param, - BadRequestException -} from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { UpdateResult } from 'typeorm'; -import { IProposal, IPagination, PermissionsEnum } from '@gauzy/contracts'; -import { ProposalService } from './proposal.service'; -import { Proposal } from './proposal.entity'; -import { CrudController, OptionParams, PaginationParams } from './../core/crud'; -import { PermissionGuard, TenantPermissionGuard } from './../shared/guards'; -import { Permissions } from './../shared/decorators'; -import { ParseJsonPipe, UUIDValidationPipe, UseValidationPipe } from './../shared/pipes'; -import { CreateProposalDTO, UpdateProposalDTO } from './dto'; - -@ApiTags('Proposal') -@UseGuards(TenantPermissionGuard, PermissionGuard) -@Permissions(PermissionsEnum.ORG_PROPOSALS_EDIT) -@Controller() -export class ProposalController extends CrudController { - constructor(private readonly proposalService: ProposalService) { - super(proposalService); - } - - /** - * GET proposal by pagination - * - * @param params - * @returns - */ - @Permissions(PermissionsEnum.ORG_PROPOSALS_VIEW) - @Get('pagination') - @UseValidationPipe({ transform: true }) - async pagination(@Query() params: PaginationParams): Promise> { - return this.proposalService.pagination(params); - } - - /** - * GET proposal by find options - * - * @param data - * @returns - */ - @ApiOperation({ summary: 'Find all proposals.' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Found proposals', - type: Proposal - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Record not found' - }) - @Permissions(PermissionsEnum.ORG_PROPOSALS_VIEW) - @Get() - async findAll(@Query('data', ParseJsonPipe) data: any): Promise> { - const { relations, findInput, filterDate } = data; - return await this.proposalService.getAllProposals({ where: findInput, relations }, filterDate); - } - - @ApiOperation({ summary: 'Find single proposal by id.' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Found proposal', - type: Proposal - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Record not found' - }) - @Permissions(PermissionsEnum.ORG_PROPOSALS_VIEW) - @Get(':id') - async findById( - @Param('id', UUIDValidationPipe) id: string, - @Query() options: OptionParams - ): Promise { - return await this.proposalService.findOneByIdString(id, { - relations: options.relations || [] - }); - } - - @ApiOperation({ summary: 'Create new record' }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: 'The record has been successfully created.' /*, type: T*/ - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'Invalid input, The response body may contain clues as to what went wrong' - }) - @Post() - @UseValidationPipe({ transform: true, whitelist: true }) - async create(@Body() entity: CreateProposalDTO): Promise { - try { - return await this.proposalService.create(entity); - } catch (error) { - throw new BadRequestException(); - } - } - - /** - * - * @param id - * @param entity - * @returns - */ - @ApiOperation({ summary: 'Update single proposal by id.' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Updates proposal', - type: Proposal - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Record not found' - }) - @Put(':id') - @UseValidationPipe({ transform: true, whitelist: true }) - async update( - @Param('id', UUIDValidationPipe) id: string, - @Body() entity: UpdateProposalDTO - ): Promise { - try { - return await this.proposalService.update(id, entity); - } catch (error) { - throw new BadRequestException(error); - } - } -} diff --git a/packages/core/src/proposal/proposal.module.ts b/packages/core/src/proposal/proposal.module.ts deleted file mode 100644 index b13570025bd..00000000000 --- a/packages/core/src/proposal/proposal.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { RouterModule } from '@nestjs/core'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { Proposal } from './proposal.entity'; -import { ProposalController } from './proposal.controller'; -import { ProposalService } from './proposal.service'; -import { Employee } from '../employee/employee.entity'; -import { CommandHandlers } from './commands/handlers'; -import { RolePermissionModule } from '../role-permission/role-permission.module'; - -@Module({ - imports: [ - RouterModule.register([{ path: '/proposal', module: ProposalModule }]), - TypeOrmModule.forFeature([Proposal, Employee]), - MikroOrmModule.forFeature([Proposal, Employee]), - RolePermissionModule - ], - controllers: [ProposalController], - providers: [ProposalService, ...CommandHandlers], - exports: [ProposalService] -}) -export class ProposalModule { } diff --git a/packages/core/src/proposal/proposal.seed.ts b/packages/core/src/proposal/proposal.seed.ts deleted file mode 100644 index 49822445a89..00000000000 --- a/packages/core/src/proposal/proposal.seed.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { DataSource } from 'typeorm'; -import { Proposal } from './proposal.entity'; -import { faker } from '@faker-js/faker'; -import * as moment from 'moment'; -import { Tag } from '../tags/tag.entity'; -import { IEmployee, IOrganization, ITenant, ProposalStatusEnum } from '@gauzy/contracts'; -import { OrganizationContact } from './../core/entities/internal'; - -export const createDefaultProposals = async ( - dataSource: DataSource, - tenant: ITenant, - employees: IEmployee[], - organizations: IOrganization[], - noOfProposalsPerOrganization: number -): Promise => { - const { id: tenantId } = tenant; - const proposals: Proposal[] = []; - for (const organization of organizations) { - const { id: organizationId } = organization; - const tags = await dataSource.manager.findBy(Tag, { - organizationId - }); - const organizationContacts = await dataSource.manager.findBy(OrganizationContact, { - organizationId, - tenantId - }); - for (let i = 0; i < noOfProposalsPerOrganization; i++) { - const proposal = new Proposal(); - proposal.employee = faker.helpers.arrayElement(employees); - proposal.jobPostUrl = faker.internet.url(); - proposal.jobPostContent = faker.person.jobTitle(); - proposal.organization = organization; - proposal.status = faker.helpers.arrayElement(Object.values(ProposalStatusEnum)); - proposal.tags = [faker.helpers.arrayElement(tags)]; - proposal.valueDate = moment(faker.date.recent({ days: 0.5 })).startOf('day').toDate(); - proposal.proposalContent = faker.person.jobDescriptor(); - proposal.tenant = tenant; - if (organizationContacts.length) { - proposal.organizationContactId = faker.helpers.arrayElement(organizationContacts).id; - } - proposals.push(proposal); - } - } - - return await dataSource.manager.save(proposals); -}; - -export const createRandomProposals = async ( - dataSource: DataSource, - tenants: ITenant[], - tenantOrganizationsMap: Map, - organizationEmployeesMap: Map, - noOfProposalsPerOrganization: number -): Promise => { - const proposals: Proposal[] = []; - for (const tenant of tenants) { - const { id: tenantId } = tenant; - const organizations = tenantOrganizationsMap.get(tenant); - for (const organization of organizations) { - const employees = organizationEmployeesMap.get(organization); - const { id: organizationId } = organization; - const tags = await dataSource.manager.findBy(Tag, { - organizationId - }); - const organizationContacts = await dataSource.manager.findBy(OrganizationContact, { - organizationId, - tenantId - }); - for (let i = 0; i < noOfProposalsPerOrganization; i++) { - const proposal = new Proposal(); - proposal.employee = faker.helpers.arrayElement(employees); - proposal.jobPostUrl = faker.internet.url(); - proposal.jobPostContent = faker.person.jobTitle(); - proposal.organization = organization; - proposal.status = faker.helpers.arrayElement(Object.values(ProposalStatusEnum)); - proposal.tags = [faker.helpers.arrayElement(tags)]; - proposal.valueDate = moment(faker.date.recent({ days: 0.5 })).startOf('day').toDate(); - proposal.proposalContent = faker.person.jobDescriptor(); - proposal.tenant = tenant; - if (organizationContacts.length) { - proposal.organizationContactId = faker.helpers.arrayElement(organizationContacts).id; - } - proposals.push(proposal); - } - } - } - - return await dataSource.manager.save(proposals); -}; diff --git a/packages/core/src/proposal/proposal.service.ts b/packages/core/src/proposal/proposal.service.ts deleted file mode 100644 index 1216ff282c6..00000000000 --- a/packages/core/src/proposal/proposal.service.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { FindManyOptions, Between, Raw } from 'typeorm'; -import * as moment from 'moment'; -import { Proposal } from './proposal.entity'; -import { IProposalCreateInput, IProposal, IPagination } from '@gauzy/contracts'; -import { isPostgres } from '@gauzy/config'; -import { TenantAwareCrudService } from './../core/crud'; -import { MikroOrmProposalRepository } from './repository/mikro-orm-proposal.repository'; -import { TypeOrmProposalRepository } from './repository/type-orm-proposal.repository'; - -@Injectable() -export class ProposalService extends TenantAwareCrudService { - constructor( - @InjectRepository(Proposal) - typeOrmProposalRepository: TypeOrmProposalRepository, - - mikroOrmProposalRepository: MikroOrmProposalRepository - ) { - super(typeOrmProposalRepository, mikroOrmProposalRepository); - } - - async getAllProposals(filter?: FindManyOptions, filterDate?: string): Promise> { - const total = await this.typeOrmRepository.count(filter); - let items = await this.typeOrmRepository.find(filter); - - if (filterDate) { - const dateObject = new Date(filterDate); - - const month = dateObject.getMonth() + 1; - const year = dateObject.getFullYear(); - - items = items.filter((i) => { - const currentItemMonth = i.valueDate.getMonth() + 1; - const currentItemYear = i.valueDate.getFullYear(); - return currentItemMonth === month && currentItemYear === year; - }); - } - - return { items, total }; - } - - async createProposal(entity: IProposalCreateInput): Promise { - const proposal = new Proposal(); - proposal.jobPostUrl = entity.jobPostUrl; - proposal.valueDate = entity.valueDate; - proposal.jobPostContent = entity.jobPostContent; - proposal.proposalContent = entity.proposalContent; - proposal.employeeId = entity.employeeId; - return this.typeOrmRepository.save(proposal); - } - - public pagination(filter: FindManyOptions) { - if ('where' in filter) { - const { where } = filter; - const likeOperator = isPostgres() ? 'ILIKE' : 'LIKE'; - if ('valueDate' in where) { - const { valueDate } = where; - const { startDate, endDate } = valueDate; - if (startDate && endDate) { - filter['where']['valueDate'] = Between( - moment.utc(startDate).format('YYYY-MM-DD HH:mm:ss'), - moment.utc(endDate).format('YYYY-MM-DD HH:mm:ss') - ); - } else { - filter['where']['valueDate'] = Between( - moment().startOf('month').utc().format('YYYY-MM-DD HH:mm:ss'), - moment().endOf('month').utc().format('YYYY-MM-DD HH:mm:ss') - ); - } - } - if ('jobPostContent' in where) { - const { jobPostContent } = where; - filter['where']['jobPostContent'] = Raw((alias) => `${alias} ${likeOperator} '%${jobPostContent}%'`); - } - } - return super.paginate(filter); - } -} diff --git a/packages/core/src/reports/commands/handlers/report-organization.create.handler.ts b/packages/core/src/reports/commands/handlers/report-organization.create.handler.ts index 3d91ddc7341..cd63200b26c 100644 --- a/packages/core/src/reports/commands/handlers/report-organization.create.handler.ts +++ b/packages/core/src/reports/commands/handlers/report-organization.create.handler.ts @@ -1,20 +1,26 @@ -import { forwardRef, Inject } from '@nestjs/common'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { ReportService } from '../../report.service'; import { ReportOrganizationCreateCommand } from '../report-organization-create.command'; +import { ReportOrganizationService } from '../../report-organization.service'; @CommandHandler(ReportOrganizationCreateCommand) export class ReportOrganizationCreateHandler implements ICommandHandler { constructor( - @Inject(forwardRef(() => ReportService)) - private readonly _reportService: ReportService - ) {} + private readonly _reportOrganizationService: ReportOrganizationService + ) { } + /** + * Executes the creation of multiple report organization entries. + * + * @param event The event containing input data for creating report organization entries. + * @returns A promise that resolves to the result of bulk creation of report organization entries. + */ public async execute(event: ReportOrganizationCreateCommand) { - const { input } = event; - return await this._reportService.bulkCreateOrganizationReport( - input - ); + try { + const { input } = event; + return await this._reportOrganizationService.bulkCreateOrganizationReport(input); + } catch (error) { + console.error(`Error occurred while executing bulk creation of report organization entries: ${error.message}`); + } } } diff --git a/packages/core/src/reports/report-category.entity.ts b/packages/core/src/reports/report-category.entity.ts index d766bc111de..dcab078e735 100644 --- a/packages/core/src/reports/report-category.entity.ts +++ b/packages/core/src/reports/report-category.entity.ts @@ -20,8 +20,15 @@ export class ReportCategory extends BaseEntity implements IReportCategory { @MultiORMColumn({ nullable: true }) iconClass?: string; - @ApiProperty({ type: () => Report }) - @MultiORMOneToMany(() => Report, (report) => report.category, { + /* + |-------------------------------------------------------------------------- + | @OneToMany + |-------------------------------------------------------------------------- + */ + /** + * + */ + @MultiORMOneToMany(() => Report, (it) => it.category, { cascade: true }) reports: IReport[]; diff --git a/packages/core/src/reports/report-organization.service.ts b/packages/core/src/reports/report-organization.service.ts new file mode 100644 index 00000000000..c49f924f096 --- /dev/null +++ b/packages/core/src/reports/report-organization.service.ts @@ -0,0 +1,80 @@ +import { BadRequestException, Injectable, } from '@nestjs/common'; +import { IOrganization, IReport, UpdateReportMenuInput } from '@gauzy/contracts'; +import { TenantAwareCrudService } from '../core/crud'; +import { RequestContext } from '../core/context'; +import { ReportOrganization } from './report-organization.entity'; +import { TypeOrmReportRepository } from './repository/type-orm-report.repository'; +import { TypeOrmReportOrganizationRepository } from './repository/type-orm-report-organization.repository'; +import { MikroOrmReportOrganizationRepository } from './repository/mikro-orm-report-organization.repository'; + +@Injectable() +export class ReportOrganizationService extends TenantAwareCrudService { + constructor( + private readonly typeOrmReportRepository: TypeOrmReportRepository, + private readonly typeOrmReportOrganizationRepository: TypeOrmReportOrganizationRepository, + private readonly mikroOrmReportOrganizationRepository: MikroOrmReportOrganizationRepository, + ) { + super(typeOrmReportOrganizationRepository, mikroOrmReportOrganizationRepository); + } + + /** + * Updates an existing report menu entry if it exists, otherwise creates a new one. + * @param input The input containing data for updating or creating the report menu entry. + * @returns The updated or newly created report menu entry. + */ + async updateReportMenu(input: UpdateReportMenuInput): Promise { + try { + const { reportId, organizationId } = input; + const tenantId = RequestContext.currentTenantId() || input.tenantId; + + let reportOrganization = await this.findOneByWhereOptions({ + reportId, + organizationId, + tenantId + }); + + // If the report organization exists, update it with the input data + reportOrganization = new ReportOrganization( + Object.assign(reportOrganization, input) + ); + return await super.save(reportOrganization); + } catch (error) { + // If the report organization doesn't exist, create a new one with the input data + return await super.create( + new ReportOrganization(input) + ); + } + } + + /** + * Bulk create organization default reports menu. + * + * @param input - The organization input data. + * @returns A promise that resolves to an array of created ReportOrganization instances. + */ + async bulkCreateOrganizationReport(input: IOrganization): Promise { + try { + const { id: organizationId, tenantId } = input; + + // Fetch reports from the database + const reports: IReport[] = await this.typeOrmReportRepository.find(); + + // Create ReportOrganization instances based on fetched reports + const reportOrganizations: ReportOrganization[] = reports.map((report: IReport) => + new ReportOrganization({ + report: { id: report.id }, + isEnabled: true, + organizationId, + tenantId + }) + ); + + // Save the created ReportOrganization instances to the database + return await this.typeOrmReportOrganizationRepository.save(reportOrganizations); + } catch (error) { + console.log(`Error occurred while attempting bulk creation of organization reports: ${error?.message}`); + // Throw InternalServerErrorException if an error occurs + throw new BadRequestException(error); + } + } +} diff --git a/packages/core/src/reports/report.controller.ts b/packages/core/src/reports/report.controller.ts index 87a25da3be9..afcb496409b 100644 --- a/packages/core/src/reports/report.controller.ts +++ b/packages/core/src/reports/report.controller.ts @@ -1,20 +1,21 @@ +import { Body, Controller, Get, HttpStatus, Post, Query } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { GetReportMenuItemsInput, IPagination, UpdateReportMenuInput, } from '@gauzy/contracts'; -import { Body, Controller, Get, HttpStatus, Post, Query } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { FindManyOptions } from 'typeorm'; import { Report } from './report.entity'; import { ReportService } from './report.service'; +import { ReportOrganizationService } from './report-organization.service'; @ApiTags('Report') @Controller() export class ReportController { constructor( - private readonly reportService: ReportService + private readonly _reportService: ReportService, + private readonly _reportOrganizationService: ReportOrganizationService, ) { } /** @@ -29,12 +30,17 @@ export class ReportController { description: 'Found records', }) @Get() - async findAll( - @Query() options: FindManyOptions + async findAllReports( + @Query() options: GetReportMenuItemsInput ): Promise> { - return await this.reportService.findAll(options); + return await this._reportService.findAllReports(options); } + /** + * + * @param filter + * @returns + */ @ApiOperation({ summary: 'Find all' }) @ApiResponse({ status: HttpStatus.OK, @@ -44,9 +50,14 @@ export class ReportController { async getMenuItems( @Query() filter?: GetReportMenuItemsInput ): Promise { - return await this.reportService.getMenuItems(filter); + return await this._reportService.getMenuItems(filter); } + /** + * + * @param input + * @returns + */ @ApiOperation({ summary: 'Find all' }) @ApiResponse({ status: HttpStatus.OK, @@ -54,6 +65,6 @@ export class ReportController { }) @Post('menu-item') async updateReportMenu(@Body() input?: UpdateReportMenuInput) { - return await this.reportService.updateReportMenu(input); + return await this._reportOrganizationService.updateReportMenu(input); } } diff --git a/packages/core/src/reports/report.entity.ts b/packages/core/src/reports/report.entity.ts index dfaf9eb2720..64b99f7aadc 100644 --- a/packages/core/src/reports/report.entity.ts +++ b/packages/core/src/reports/report.entity.ts @@ -57,8 +57,6 @@ export class Report extends BaseEntity implements IReport { | @ManyToOne |-------------------------------------------------------------------------- */ - - @ApiProperty({ type: () => ReportCategory }) @MultiORMManyToOne(() => ReportCategory, (it) => it.reports, { onDelete: 'CASCADE' }) @@ -74,11 +72,9 @@ export class Report extends BaseEntity implements IReport { /* |-------------------------------------------------------------------------- - | @ManyToOne + | @OneToMany |-------------------------------------------------------------------------- */ - - @ApiProperty({ type: () => ReportOrganization }) @MultiORMOneToMany(() => ReportOrganization, (it) => it.report, { cascade: true }) diff --git a/packages/core/src/reports/report.module.ts b/packages/core/src/reports/report.module.ts index cbff785f936..0dadc54a56f 100644 --- a/packages/core/src/reports/report.module.ts +++ b/packages/core/src/reports/report.module.ts @@ -10,6 +10,9 @@ import { ReportCategoryController } from './report-category.controller'; import { ReportCategoryService } from './report-category.service'; import { ReportOrganization } from './report-organization.entity'; import { CommandHandlers } from './commands/handlers'; +import { ReportOrganizationService } from './report-organization.service'; +import { TypeOrmReportOrganizationRepository } from './repository/type-orm-report-organization.repository'; +import { TypeOrmReportRepository } from './repository/type-orm-report.repository'; @Module({ imports: [ @@ -23,6 +26,13 @@ import { CommandHandlers } from './commands/handlers'; MikroOrmModule.forFeature([Report, ReportCategory, ReportOrganization]) ], controllers: [ReportCategoryController, ReportController], - providers: [ReportService, ReportCategoryService, ...CommandHandlers] + providers: [ + ReportService, + ReportCategoryService, + ReportOrganizationService, + TypeOrmReportRepository, + TypeOrmReportOrganizationRepository, + ...CommandHandlers + ] }) export class ReportModule { } diff --git a/packages/core/src/reports/report.service.ts b/packages/core/src/reports/report.service.ts index a2b75083289..f872694b417 100644 --- a/packages/core/src/reports/report.service.ts +++ b/packages/core/src/reports/report.service.ts @@ -1,24 +1,14 @@ +import { Injectable, Logger, } from '@nestjs/common'; import { GetReportMenuItemsInput, - IOrganization, IPagination, - IReport, - UpdateReportMenuInput, + IReport } from '@gauzy/contracts'; -import { - Injectable, - InternalServerErrorException, - Logger, -} from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { indexBy } from 'underscore'; import { CrudService } from '../core/crud'; import { RequestContext } from './../core/context'; -import { ReportOrganization } from './report-organization.entity'; import { Report } from './report.entity'; -import { TypeOrmReportRepository } from './repository/type-orm-report.repository'; import { MikroOrmReportRepository } from './repository/mikro-orm-report.repository'; -import { TypeOrmReportOrganizationRepository } from './repository/type-orm-report-organization.repository'; +import { TypeOrmReportRepository } from './repository/type-orm-report.repository'; @Injectable() export class ReportService extends CrudService { @@ -26,124 +16,70 @@ export class ReportService extends CrudService { private readonly logger = new Logger(ReportService.name); constructor( - @InjectRepository(Report) - typeOrmReportRepository: TypeOrmReportRepository, - - mikroOrmReportRepository: MikroOrmReportRepository, - - @InjectRepository(ReportOrganization) - readonly typeOrmReportOrganizationRepository: TypeOrmReportOrganizationRepository + readonly typeOrmReportRepository: TypeOrmReportRepository, + readonly mikroOrmReportRepository: MikroOrmReportRepository ) { super(typeOrmReportRepository, mikroOrmReportRepository); } - public async findAll(filter?: any): Promise> { - const start = new Date(); - - const { items, total } = await super.findAll(filter); - const menuItems = await this.getMenuItems(filter); - - const orgMenuItems = indexBy(menuItems, 'id'); - - const mapItems = items.map((item) => { - if (orgMenuItems[item.id]) { - item.showInMenu = true; - } else { - item.showInMenu = false; - } - return item; - }); - - const end = new Date(); - const time = (end.getTime() - start.getTime()) / 1000; - - this.logger.log(`ReportService.findAll took ${time} seconds`); - console.log(`ReportService.findAll took ${time} seconds`); - - return { items: mapItems, total }; - } - /** - * Get reports menus + * Retrieves all reports for the specified organization and tenant, including whether they should be shown in the menu. * - * @param options - * @returns + * @param filter The filter containing organization ID and tenant ID for retrieving reports. + * @returns A promise that resolves to an object containing paginated report items and total count. */ - public async getMenuItems( - options: GetReportMenuItemsInput - ): Promise { - const { organizationId } = options; - const tenantId = RequestContext.currentTenantId() || options.tenantId; - - return await this.typeOrmRepository.find({ - join: { - alias: this.tableName, - innerJoin: { - reportOrganizations: `${this.tableName}.reportOrganizations`, - } - }, - where: { - reportOrganizations: { - organizationId, - tenantId, - isEnabled: true - } - } + public async findAllReports(filter?: any): Promise> { + console.time(`ReportService.findAll took seconds`); + // Extract organizationId and tenantId from filter + const { organizationId } = filter; + const tenantId = RequestContext.currentTenantId() || filter.tenantId; + + // Fetch all reports and their associated organizations in a single query + const qb = this.typeOrmRepository.createQueryBuilder('report'); + qb.setFindOptions({ + ...(filter.relations ? { relations: filter.relations } : {}) + }); + qb.leftJoinAndSelect('report.reportOrganizations', 'ro', 'ro.organizationId = :organizationId AND ro.tenantId = :tenantId AND ro.isEnabled = :isEnabled AND ro.isActive = :isActive AND ro.isArchived = :isArchived', { + organizationId, + tenantId, + isEnabled: true, + isActive: true, + isArchived: false }); - } - async updateReportMenu( - input: UpdateReportMenuInput - ): Promise { - let reportOrganization = - await this.typeOrmReportOrganizationRepository.findOne({ - where: { - reportId: input.reportId, - }, - }); + // Execute the query + const [items, total] = await qb.getManyAndCount(); - if (!reportOrganization) { - reportOrganization = new ReportOrganization(input); - } else { - reportOrganization = new ReportOrganization( - Object.assign(reportOrganization, input) - ); - } + // Map over items and set 'showInMenu' property based on menu item existence + const reports = items.map((item) => { + item.showInMenu = !!item.reportOrganizations.length; // true if there are reportOrganizations, false otherwise + delete item.reportOrganizations; // Remove reportOrganizations from the report object + return item; + }); - this.typeOrmReportOrganizationRepository.save(reportOrganization); - return reportOrganization; + console.timeEnd(`ReportService.findAll took seconds`); + return { items: reports, total: total }; } /** - * Bulk create organization default reports menu. + * Retrieves report menu items based on the provided options. * - * @param input - The organization input data. - * @returns A promise that resolves to an array of created ReportOrganization instances. + * @param input The input containing the organization ID and tenant ID for filtering report menu items. + * @returns A promise that resolves to an array of report menu items. */ - async bulkCreateOrganizationReport(input: IOrganization): Promise { - try { - const { id: organizationId, tenantId } = input; - - // Fetch reports from the database - const reports: IReport[] = await super.find(); // Replace 'super' with your appropriate superclass or service - - // Create ReportOrganization instances based on fetched reports - const reportOrganizations: ReportOrganization[] = reports.map((report: IReport) => - new ReportOrganization({ - report, - organizationId, - tenantId - }) - ); - - // Save the created ReportOrganization instances to the database - await this.typeOrmReportOrganizationRepository.save(reportOrganizations); + public async getMenuItems(input: GetReportMenuItemsInput): Promise { + const { organizationId } = input; + const tenantId = RequestContext.currentTenantId() || input.tenantId; + + const qb = this.typeOrmRepository.createQueryBuilder('report'); + qb.innerJoin('report.reportOrganizations', 'ro', 'ro.isEnabled = :isEnabled AND ro.isActive = :isActive AND ro.isArchived = :isArchived', { + isEnabled: true, + isActive: true, + isArchived: false + }); + qb.andWhere('ro.organizationId = :organizationId', { organizationId }); + qb.andWhere('ro.tenantId = :tenantId', { tenantId }); - // Return the array of created ReportOrganization instances - return reportOrganizations; - } catch (error) { - // Throw InternalServerErrorException if an error occurs - throw new InternalServerErrorException(error); - } + return await qb.getMany(); } } diff --git a/packages/core/src/tags/dto/relational-tag.dto.ts b/packages/core/src/tags/dto/relational-tag.dto.ts index 2ea1d9a45ad..9e52804d6a4 100644 --- a/packages/core/src/tags/dto/relational-tag.dto.ts +++ b/packages/core/src/tags/dto/relational-tag.dto.ts @@ -1,10 +1,11 @@ -import { ITag } from "@gauzy/contracts"; import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional } from "class-validator"; +import { IsArray, IsOptional } from "class-validator"; +import { ITag } from "@gauzy/contracts"; export class RelationalTagDTO { @ApiPropertyOptional({ type: () => Array, isArray: true }) @IsOptional() + @IsArray() readonly tags: ITag[]; -} \ No newline at end of file +} diff --git a/packages/core/src/tags/index.ts b/packages/core/src/tags/index.ts new file mode 100644 index 00000000000..82cd8055ac1 --- /dev/null +++ b/packages/core/src/tags/index.ts @@ -0,0 +1,2 @@ +export * from './tag.types'; +export * from './dto'; \ No newline at end of file diff --git a/packages/core/src/tags/tag.controller.ts b/packages/core/src/tags/tag.controller.ts index b3de1f61034..44786ec8329 100644 --- a/packages/core/src/tags/tag.controller.ts +++ b/packages/core/src/tags/tag.controller.ts @@ -57,7 +57,9 @@ export class TagController extends CrudController { @Get() @UseValidationPipe() async findAll(@Query() options: PaginationParams): Promise { - return await this.commandBus.execute(new TagListCommand(options.where, options.relations)); + return await this.commandBus.execute( + new TagListCommand(options.where, options.relations) + ); } /** diff --git a/packages/core/src/tags/tag.entity.ts b/packages/core/src/tags/tag.entity.ts index caef6f93a3f..457097a6157 100644 --- a/packages/core/src/tags/tag.entity.ts +++ b/packages/core/src/tags/tag.entity.ts @@ -2,6 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { RelationId } from 'typeorm'; import { EntityRepositoryType } from '@mikro-orm/core'; import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; +import { CustomEmbeddedFields } from '@gauzy/common'; import { ICandidate, IEmployee, @@ -24,7 +25,6 @@ import { IOrganizationVendor, IPayment, IProduct, - IProposal, IRequestApproval, ITag, ITask, @@ -53,14 +53,14 @@ import { OrganizationVendor, Payment, Product, - Proposal, RequestApproval, Task, TenantOrganizationBaseEntity, User, Warehouse } from '../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToMany, MultiORMManyToOne } from './../core/decorators/entity'; +import { CustomTagFields } from '../core/entities/custom-entity-fields/custom-entity-fields'; +import { ColumnIndex, EmbeddedColumn, MultiORMColumn, MultiORMEntity, MultiORMManyToMany, MultiORMManyToOne, VirtualMultiOrmColumn } from '../core/decorators/entity'; import { MikroOrmTagRepository } from './repository/mikro-orm-tag.repository'; @MultiORMEntity('tag', { mikroOrmRepository: () => MikroOrmTagRepository }) @@ -101,6 +101,7 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { @MultiORMColumn({ default: false }) isSystem?: boolean; + @VirtualMultiOrmColumn() fullIconUrl?: string; /* @@ -207,15 +208,6 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { }) tasks?: ITask[]; - /** - * Proposal - */ - @MultiORMManyToMany(() => Proposal, (it) => it.tags, { - /** Defines the database cascade action on delete. */ - onDelete: 'CASCADE' - }) - proposals?: IProposal[]; - /** * OrganizationVendor */ @@ -368,4 +360,12 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { onDelete: 'CASCADE' }) organizations?: IOrganization[]; + + /* + |-------------------------------------------------------------------------- + | Embeddable Columns + |-------------------------------------------------------------------------- + */ + @EmbeddedColumn(() => CustomTagFields, { prefix: false }) + customFields?: CustomEmbeddedFields; } diff --git a/packages/core/src/tags/tag.service.ts b/packages/core/src/tags/tag.service.ts index abe8262444e..22c6bdb6503 100644 --- a/packages/core/src/tags/tag.service.ts +++ b/packages/core/src/tags/tag.service.ts @@ -75,11 +75,7 @@ export class TagService extends TenantAwareCrudService { * Defines a special criteria to find specific relations. */ query.setFindOptions({ - ...(relations - ? { - relations: relations - } - : {}) + ...(relations ? { relations: relations } : {}) }); /** * Left join all relational tables with tag table @@ -105,41 +101,47 @@ export class TagService extends TenantAwareCrudService { query.leftJoin(`${query.alias}.organizationVendors`, 'organizationVendor'); query.leftJoin(`${query.alias}.payments`, 'payment'); query.leftJoin(`${query.alias}.products`, 'product'); - query.leftJoin(`${query.alias}.proposals`, 'proposal'); query.leftJoin(`${query.alias}.requestApprovals`, 'requestApproval'); query.leftJoin(`${query.alias}.tasks`, 'task'); query.leftJoin(`${query.alias}.users`, 'user'); query.leftJoin(`${query.alias}.warehouses`, 'warehouse'); + + /** + * Custom Entity Fields + */ + query.leftJoin(`${query.alias}.customFields.proposals`, 'proposal'); + /** * Adds new selection to the SELECT query. */ query.select(`${query.alias}.*`); - query.addSelect(p(`COUNT("candidate"."id")`), `candidate_counter`); - query.addSelect(p(`COUNT("employee"."id")`), `employee_counter`); - query.addSelect(p(`COUNT("employeeLevel"."id")`), `employee_level_counter`); - query.addSelect(p(`COUNT("equipment"."id")`), `equipment_counter`); - query.addSelect(p(`COUNT("eventType"."id")`), `event_type_counter`); - query.addSelect(p(`COUNT("expense"."id")`), `expense_counter`); - query.addSelect(p(`COUNT("income"."id")`), `income_counter`); - query.addSelect(p(`COUNT("integration"."id")`), `integration_counter`); - query.addSelect(p(`COUNT("invoice"."id")`), `invoice_counter`); - query.addSelect(p(`COUNT("merchant"."id")`), `merchant_counter`); - query.addSelect(p(`COUNT("organization"."id")`), `organization_counter`); - query.addSelect(p(`COUNT("organizationContact"."id")`), `organization_contact_counter`); - query.addSelect(p(`COUNT("organizationDepartment"."id")`), `organization_department_counter`); - query.addSelect(p(`COUNT("organizationEmploymentType"."id")`), `organization_employment_type_counter`); - query.addSelect(p(`COUNT("expenseCategory"."id")`), `expense_category_counter`); - query.addSelect(p(`COUNT("organizationPosition"."id")`), `organization_position_counter`); - query.addSelect(p(`COUNT("organizationProject"."id")`), `organization_project_counter`); - query.addSelect(p(`COUNT("organizationTeam"."id")`), `organization_team_counter`); - query.addSelect(p(`COUNT("organizationVendor"."id")`), `organization_vendor_counter`); - query.addSelect(p(`COUNT("payment"."id")`), `payment_counter`); - query.addSelect(p(`COUNT("product"."id")`), `product_counter`); - query.addSelect(p(`COUNT("proposal"."id")`), `proposal_counter`); - query.addSelect(p(`COUNT("requestApproval"."id")`), `request_approval_counter`); - query.addSelect(p(`COUNT("task"."id")`), `task_counter`); - query.addSelect(p(`COUNT("user"."id")`), `user_counter`); - query.addSelect(p(`COUNT("warehouse"."id")`), `warehouse_counter`); + // Add the select statement for counting, and cast it to integer + query.addSelect(p(`CAST(COUNT("candidate"."id") AS INTEGER)`), `candidate_counter`); + query.addSelect(p(`CAST(COUNT("employee"."id") AS INTEGER)`), `employee_counter`); + query.addSelect(p(`CAST(COUNT("employeeLevel"."id") AS INTEGER)`), `employee_level_counter`); + query.addSelect(p(`CAST(COUNT("equipment"."id") AS INTEGER)`), `equipment_counter`); + query.addSelect(p(`CAST(COUNT("eventType"."id") AS INTEGER)`), `event_type_counter`); + query.addSelect(p(`CAST(COUNT("expense"."id") AS INTEGER)`), `expense_counter`); + query.addSelect(p(`CAST(COUNT("income"."id") AS INTEGER)`), `income_counter`); + query.addSelect(p(`CAST(COUNT("integration"."id") AS INTEGER)`), `integration_counter`); + query.addSelect(p(`CAST(COUNT("invoice"."id") AS INTEGER)`), `invoice_counter`); + query.addSelect(p(`CAST(COUNT("merchant"."id") AS INTEGER)`), `merchant_counter`); + query.addSelect(p(`CAST(COUNT("organization"."id") AS INTEGER)`), `organization_counter`); + query.addSelect(p(`CAST(COUNT("organizationContact"."id") AS INTEGER)`), `organization_contact_counter`); + query.addSelect(p(`CAST(COUNT("organizationDepartment"."id") AS INTEGER)`), `organization_department_counter`); + query.addSelect(p(`CAST(COUNT("organizationEmploymentType"."id") AS INTEGER)`), `organization_employment_type_counter`); + query.addSelect(p(`CAST(COUNT("expenseCategory"."id") AS INTEGER)`), `expense_category_counter`); + query.addSelect(p(`CAST(COUNT("organizationPosition"."id") AS INTEGER)`), `organization_position_counter`); + query.addSelect(p(`CAST(COUNT("organizationProject"."id") AS INTEGER)`), `organization_project_counter`); + query.addSelect(p(`CAST(COUNT("organizationTeam"."id") AS INTEGER)`), `organization_team_counter`); + query.addSelect(p(`CAST(COUNT("organizationVendor"."id") AS INTEGER)`), `organization_vendor_counter`); + query.addSelect(p(`CAST(COUNT("payment"."id") AS INTEGER)`), `payment_counter`); + query.addSelect(p(`CAST(COUNT("product"."id") AS INTEGER)`), `product_counter`); + query.addSelect(p(`CAST(COUNT("requestApproval"."id") AS INTEGER)`), `request_approval_counter`); + query.addSelect(p(`CAST(COUNT("task"."id") AS INTEGER)`), `task_counter`); + query.addSelect(p(`CAST(COUNT("user"."id") AS INTEGER)`), `user_counter`); + query.addSelect(p(`CAST(COUNT("warehouse"."id") AS INTEGER)`), `warehouse_counter`); + query.addSelect(p(`CAST(COUNT("proposal"."id") AS INTEGER)`), `proposal_counter`); /** * Adds GROUP BY condition in the query builder. */ diff --git a/packages/core/src/tags/tag.types.ts b/packages/core/src/tags/tag.types.ts new file mode 100644 index 00000000000..5e00c6aceae --- /dev/null +++ b/packages/core/src/tags/tag.types.ts @@ -0,0 +1,11 @@ +import { Tag } from "./tag.entity"; + +/** + * Represents an entity that can be tagged with multiple tags. + */ +export interface Taggable { + /** + * An array of tags associated with the entity. + */ + tags?: Tag[]; +} diff --git a/packages/core/src/tasks/task.controller.ts b/packages/core/src/tasks/task.controller.ts index f9eb2b9c5a1..03414b0bd62 100644 --- a/packages/core/src/tasks/task.controller.ts +++ b/packages/core/src/tasks/task.controller.ts @@ -9,9 +9,7 @@ import { Body, UseGuards, Post, - Delete, - ValidationPipe, - UsePipes + Delete } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { CommandBus } from '@nestjs/cqrs'; diff --git a/packages/core/src/tenant/tenant.controller.ts b/packages/core/src/tenant/tenant.controller.ts index c6f0f7c4aca..a2483671fd9 100644 --- a/packages/core/src/tenant/tenant.controller.ts +++ b/packages/core/src/tenant/tenant.controller.ts @@ -41,7 +41,8 @@ export class TenantController { }) @Get() async findById(): Promise { - return await this.tenantService.findOneByIdString(RequestContext.currentTenantId()); + const tenantId = RequestContext.currentTenantId(); + return await this.tenantService.findOneByIdString(tenantId); } /** diff --git a/packages/core/src/tenant/tenant.service.ts b/packages/core/src/tenant/tenant.service.ts index 0f67138c307..a2152ec5232 100644 --- a/packages/core/src/tenant/tenant.service.ts +++ b/packages/core/src/tenant/tenant.service.ts @@ -25,15 +25,12 @@ import { Tenant } from './tenant.entity'; @Injectable() export class TenantService extends CrudService { constructor( - readonly typeOrmTenantRepository: TypeOrmTenantRepository, - readonly mikroOrmTenantRepository: MikroOrmTenantRepository, - - readonly typeOrmRoleRepository: TypeOrmRoleRepository, - readonly mikroOrmRoleRepository: MikroOrmRoleRepository, - - readonly typeOrmUserRepository: TypeOrmUserRepository, - readonly mikroOrmUserRepository: MikroOrmUserRepository, - + private readonly typeOrmTenantRepository: TypeOrmTenantRepository, + private readonly mikroOrmTenantRepository: MikroOrmTenantRepository, + private readonly typeOrmRoleRepository: TypeOrmRoleRepository, + private readonly mikroOrmRoleRepository: MikroOrmRoleRepository, + private readonly typeOrmUserRepository: TypeOrmUserRepository, + private readonly mikroOrmUserRepository: MikroOrmUserRepository, private readonly commandBus: CommandBus, private readonly configService: ConfigService ) { diff --git a/packages/core/src/time-tracking/activity/activity.module.ts b/packages/core/src/time-tracking/activity/activity.module.ts index 00779e03408..505a5711f65 100644 --- a/packages/core/src/time-tracking/activity/activity.module.ts +++ b/packages/core/src/time-tracking/activity/activity.module.ts @@ -11,6 +11,7 @@ import { ActivityService } from './activity.service'; import { Activity } from './activity.entity'; import { ActivityMapService } from './activity.map.service'; import { TimeSlotModule } from './../time-slot/time-slot.module'; +import { TypeOrmActivityRepository } from './repository'; @Module({ controllers: [ @@ -28,8 +29,9 @@ import { TimeSlotModule } from './../time-slot/time-slot.module'; providers: [ ActivityService, ActivityMapService, + TypeOrmActivityRepository, ...CommandHandlers ], - exports: [TypeOrmModule, MikroOrmModule, ActivityService, ActivityMapService] + exports: [TypeOrmModule, MikroOrmModule, ActivityService, ActivityMapService, TypeOrmActivityRepository] }) export class ActivityModule { } diff --git a/packages/core/src/time-tracking/activity/repository/index.ts b/packages/core/src/time-tracking/activity/repository/index.ts new file mode 100644 index 00000000000..3be537625d6 --- /dev/null +++ b/packages/core/src/time-tracking/activity/repository/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-activity.repository'; +export * from './type-orm-activity.repository'; diff --git a/packages/core/src/time-tracking/statistic/statistic.helper.ts b/packages/core/src/time-tracking/statistic/statistic.helper.ts index 4e03e017b88..c07f46ce74c 100644 --- a/packages/core/src/time-tracking/statistic/statistic.helper.ts +++ b/packages/core/src/time-tracking/statistic/statistic.helper.ts @@ -1,4 +1,5 @@ import { DatabaseTypeEnum } from "@gauzy/config"; +import { prepareSQLQuery as p } from './../../database/database.helper'; /** * Builds a SELECT statement for the "user_name" column based on the database type. @@ -24,3 +25,68 @@ export function concateUserNameExpression(dbType: string): string { return expression; } + +/** + * Generates a SQL query string for calculating the total duration of tasks today. + * + * @param dbType The type of database (e.g., 'sqlite', 'postgres', 'mysql', etc.). + * @param queryAlias The alias used for the table in the SQL query. + * @returns The SQL query string for calculating task duration for Today. + */ +export const getTasksTodayDurationQueryString = (dbType: string, queryAlias: string) => { + switch (dbType) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + return `COALESCE(ROUND(SUM((julianday(COALESCE("${queryAlias}"."stoppedAt", datetime('now'))) - julianday("${queryAlias}"."startedAt")) * 86400) / COUNT("time_slot"."id")), 0)`; + case DatabaseTypeEnum.postgres: + return `COALESCE(ROUND(SUM(extract(epoch from (COALESCE("${queryAlias}"."stoppedAt", NOW()) - "${queryAlias}"."startedAt"))) / COUNT("time_slot"."id")), 0)`; + case DatabaseTypeEnum.mysql: + // Directly return the SQL string for MySQL, as MikroORM allows raw SQL. + return p(`COALESCE(ROUND(SUM(TIMESTAMPDIFF(SECOND, "${queryAlias}"."startedAt", COALESCE("${queryAlias}"."stoppedAt", NOW()))) / COUNT("time_slot"."id")), 0)`); + default: + throw new Error(`Unsupported database type: ${dbType}`); + } +}; + +/** + * Generates SQL query string for task duration based on database type and query variation. + * + * @param dbType The type of database (e.g., 'sqlite', 'postgres', 'mysql', etc.). + * @param queryAlias The alias used for the table in the SQL query. + * @returns The SQL query string for calculating task duration. + */ +export const getTasksDurationQueryString = (dbType: string, queryAlias: string) => { + switch (dbType) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + return `COALESCE(ROUND(SUM((julianday(COALESCE("${queryAlias}"."stoppedAt", datetime('now'))) - julianday("${queryAlias}"."startedAt")) * 86400) / COUNT("time_slot"."id")), 0)`; + case DatabaseTypeEnum.postgres: + return `COALESCE(ROUND(SUM(extract(epoch from (COALESCE("${queryAlias}"."stoppedAt", NOW()) - "${queryAlias}"."startedAt"))) / COUNT("time_slot"."id")), 0)`; + case DatabaseTypeEnum.mysql: + return `COALESCE(ROUND(SUM(TIMESTAMPDIFF(SECOND, "${queryAlias}"."startedAt", COALESCE("${queryAlias}"."stoppedAt", NOW()))) / COUNT("time_slot"."id")), 0)`; + default: + throw new Error(`Unsupported database type: ${dbType}`); + } +}; + +/** + * Generates a SQL query string for calculating the total duration of tasks across all time. + * The query varies depending on the database type. + * + * @param dbType The type of the database (e.g., SQLite, PostgreSQL, MySQL). + * @param queryAlias The alias used for the table in the SQL query. + * @returns The SQL query string for calculating task total duration. + */ +export const getTasksTotalDurationQueryString = (dbType: string, queryAlias: string): string => { + switch (dbType) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + return `COALESCE(ROUND(SUM((julianday(COALESCE("${queryAlias}"."stoppedAt", datetime('now'))) - julianday("${queryAlias}"."startedAt")) * 86400)), 0)`; + case DatabaseTypeEnum.postgres: + return `COALESCE(ROUND(SUM(extract(epoch from (COALESCE("${queryAlias}"."stoppedAt", NOW()) - "${queryAlias}"."startedAt")))), 0)`; + case DatabaseTypeEnum.mysql: + return `COALESCE(ROUND(SUM(TIMESTAMPDIFF(SECOND, "${queryAlias}"."startedAt", COALESCE("${queryAlias}"."stoppedAt", NOW())))), 0)`; + default: + throw Error(`Unsupported database type: ${dbType}`); + } +}; diff --git a/packages/core/src/time-tracking/statistic/statistic.service.ts b/packages/core/src/time-tracking/statistic/statistic.service.ts index 2e9436ae7c3..408e80a80d2 100644 --- a/packages/core/src/time-tracking/statistic/statistic.service.ts +++ b/packages/core/src/time-tracking/statistic/statistic.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; import { Brackets, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'; import { reduce, pluck, pick, mapObject, groupBy, chain } from 'underscore'; import * as _ from 'underscore'; import * as moment from 'moment'; +import * as chalk from 'chalk'; import { PermissionsEnum, IGetActivitiesStatistics, @@ -20,50 +20,40 @@ import { IGetManualTimesStatistics, IManualTimesStatistics, TimeLogType, - ITask, ITimeLog } from '@gauzy/contracts'; import { ArraySum, isNotEmpty } from '@gauzy/common'; -import { ConfigService, DatabaseTypeEnum, isBetterSqlite3, isMySQL, isPostgres, isSqlite } from '@gauzy/config'; -import { concateUserNameExpression } from './statistic.helper'; +import { ConfigService, DatabaseTypeEnum, MultiORM, isBetterSqlite3, isMySQL, isPostgres, isSqlite } from '@gauzy/config'; +import { concateUserNameExpression, getTasksDurationQueryString, getTasksTodayDurationQueryString, getTasksTotalDurationQueryString } from './statistic.helper'; import { prepareSQLQuery as p } from './../../database/database.helper'; import { RequestContext } from '../../core/context'; -import { Activity, Employee, TimeLog, TimeSlot } from './../../core/entities/internal'; -import { getDateRangeFormat } from './../../core/utils'; +import { TimeLog, TimeSlot } from './../../core/entities/internal'; +import { MultiORMEnum, getDateRangeFormat, getORMType } from './../../core/utils'; import { TypeOrmTimeSlotRepository } from '../../time-tracking/time-slot/repository/type-orm-time-slot.repository'; import { MikroOrmTimeSlotRepository } from '../../time-tracking/time-slot/repository/mikro-orm-time-slot.repository'; import { TypeOrmEmployeeRepository } from '../../employee/repository/type-orm-employee.repository'; import { MikroOrmEmployeeRepository } from '../../employee/repository/mikro-orm-employee.repository'; -import { TypeOrmActivityRepository } from '../activity/repository/type-orm-activity.repository'; -import { MikroOrmActivityRepository } from '../activity/repository/mikro-orm-activity.repository'; -import { TypeOrmTimeLogRepository } from '../time-log/repository/type-orm-time-log.repository'; -import { MikroOrmTimeLogRepository } from '../time-log/repository/mikro-orm-time-log.repository'; +import { MikroOrmActivityRepository, TypeOrmActivityRepository } from '../activity/repository'; +import { MikroOrmTimeLogRepository, TypeOrmTimeLogRepository } from '../time-log/repository'; + +// Get the type of the Object-Relational Mapping (ORM) used in the application. +const ormType: MultiORM = getORMType(); @Injectable() export class StatisticService { + protected ormType: MultiORM = ormType; + constructor( - @InjectRepository(TimeSlot) private readonly typeOrmTimeSlotRepository: TypeOrmTimeSlotRepository, - - readonly mikroOrmTimeSlotRepository: MikroOrmTimeSlotRepository, - - @InjectRepository(Employee) + private readonly mikroOrmTimeSlotRepository: MikroOrmTimeSlotRepository, private readonly typeOrmEmployeeRepository: TypeOrmEmployeeRepository, - - readonly mikroEmployeeRepository: MikroOrmEmployeeRepository, - - @InjectRepository(Activity) + private readonly mikroEmployeeRepository: MikroOrmEmployeeRepository, private readonly typeOrmActivityRepository: TypeOrmActivityRepository, - - readonly mikroOrmActivityRepository: MikroOrmActivityRepository, - - @InjectRepository(TimeLog) + private readonly mikroOrmActivityRepository: MikroOrmActivityRepository, private readonly typeOrmTimeLogRepository: TypeOrmTimeLogRepository, - - readonly mikroOrmTimeLogRepository: MikroOrmTimeLogRepository, - + private readonly mikroOrmTimeLogRepository: MikroOrmTimeLogRepository, private readonly configService: ConfigService - ) {} + ) { } /** * GET Time Tracking Dashboard Counts Statistics @@ -732,10 +722,10 @@ export class StatisticService { isSqlite() || isBetterSqlite3() ? `(strftime('%w', timeLogs.startedAt))` : isPostgres() - ? 'EXTRACT(DOW FROM "timeLogs"."startedAt")' - : isMySQL() - ? p('DayOfWeek("timeLogs"."startedAt") - 1') - : '0', + ? 'EXTRACT(DOW FROM "timeLogs"."startedAt")' + : isMySQL() + ? p('DayOfWeek("timeLogs"."startedAt") - 1') + : '0', 'day' ) .andWhere(p(`"${weekHoursQuery.alias}"."id" = :memberId`), { memberId: member.id }) @@ -783,10 +773,10 @@ export class StatisticService { isSqlite() || isBetterSqlite3() ? `(strftime('%w', timeLogs.startedAt))` : isPostgres() - ? 'EXTRACT(DOW FROM "timeLogs"."startedAt")' - : isMySQL() - ? p('DayOfWeek("timeLogs"."startedAt") - 1') - : '0' + ? 'EXTRACT(DOW FROM "timeLogs"."startedAt")' + : isMySQL() + ? p('DayOfWeek("timeLogs"."startedAt") - 1') + : '0' ); member.weekHours = await weekHoursQuery.getRawMany(); @@ -994,15 +984,8 @@ export class StatisticService { */ async getTasks(request: IGetTasksStatistics) { const { organizationId, startDate, endDate, take, onlyMe = false, organizationTeamId } = request; - let { - employeeIds = [], - projectIds = [], - taskIds = [], - defaultRange, - unitOfTime, - todayEnd, - todayStart - } = request; + const { projectIds = [], taskIds = [], defaultRange, unitOfTime } = request; + let { employeeIds = [], todayEnd, todayStart } = request; const user = RequestContext.currentUser(); const tenantId = RequestContext.currentTenantId() || request.tenantId; @@ -1011,22 +994,20 @@ export class StatisticService { let end: string | Date; if (startDate && endDate) { - const range = getDateRangeFormat(moment.utc(startDate), moment.utc(endDate)); + const range = getDateRangeFormat( + moment.utc(startDate), + moment.utc(endDate) + ); + start = range.start; + end = range.end; + } else if (defaultRange) { + const unit = unitOfTime || 'week'; + const range = getDateRangeFormat( + moment().startOf(unit).utc(), + moment().endOf(unit).utc() + ); start = range.start; end = range.end; - } else { - if (typeof defaultRange === 'boolean' && defaultRange) { - const range = getDateRangeFormat( - moment() - .startOf(unitOfTime || 'week') - .utc(), - moment() - .endOf(unitOfTime || 'week') - .utc() - ); - start = range.start; - end = range.end; - } } /* @@ -1048,270 +1029,331 @@ export class StatisticService { } if (todayStart && todayEnd) { - const range = getDateRangeFormat(moment.utc(todayStart), moment.utc(todayEnd)); + const range = getDateRangeFormat( + moment.utc(todayStart), + moment.utc(todayEnd) + ); + todayStart = range.start; + todayEnd = range.end; + } else if (defaultRange) { + const unit = unitOfTime || 'day'; + const range = getDateRangeFormat( + moment().startOf(unit).utc(), + moment().endOf(unit).utc() + ); todayStart = range.start; todayEnd = range.end; - } else { - if (typeof defaultRange === 'boolean' && defaultRange) { - const range = getDateRangeFormat( - moment() - .startOf(unitOfTime || 'week') - .utc(), - moment() - .endOf(unitOfTime || 'week') - .utc() - ); - todayStart = range.start; - todayEnd = range.end; - } } - const todayQuery = this.typeOrmTimeLogRepository.createQueryBuilder(); + // Retrieves the database type from the configuration service. + const dbType = this.configService.dbConnectionOptions.type; - let todayQueryString: string; - switch (this.configService.dbConnectionOptions.type) { - case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: - todayQueryString = `COALESCE(ROUND(SUM((julianday(COALESCE("${todayQuery.alias}"."stoppedAt", datetime('now'))) - julianday("${todayQuery.alias}"."startedAt")) * 86400) / COUNT("time_slot"."id")), 0)`; - break; - case DatabaseTypeEnum.postgres: - todayQueryString = `COALESCE(ROUND(SUM(extract(epoch from (COALESCE("${todayQuery.alias}"."stoppedAt", NOW()) - "${todayQuery.alias}"."startedAt"))) / COUNT("time_slot"."id")), 0)`; + let todayStatistics: any[] = []; + + /** + * Get Today's Task Statistics + */ + switch (this.ormType) { + case MultiORMEnum.MikroORM: { + // Start building the MikroORM query + const qb = this.mikroOrmTimeLogRepository.createQueryBuilder('time_log'); + const knex = this.mikroOrmTimeLogRepository.getKnex(); + + // Add the raw SQL snippet to the select + const raw = getTasksTodayDurationQueryString(dbType, qb.alias); + + // Constructs SQL query to fetch task title, ID, last updated timestamp, and today's duration. + let sq = knex(qb.alias).select([ + `task.title AS title`, + `task.id AS taskId`, + `${qb.alias}.updatedAt AS updatedAt`, + knex.raw(`${raw} AS today_duration`), + ]); + + // Add join clauses + sq.innerJoin('task', `${qb.alias}.taskId`, 'task.id'); + sq.innerJoin('time_slot_time_logs', `${qb.alias}.id`, 'time_slot_time_logs.timeLogId'); + sq.innerJoin('time_slot', 'time_slot_time_logs.timeSlotId', 'time_slot.id'); + + // Add where clauses + sq.andWhere({ + [`${qb.alias}.tenantId`]: tenantId, + [`${qb.alias}.organizationId`]: organizationId, + [`time_slot.tenantId`]: tenantId, + [`time_slot.organizationId`]: organizationId + }); + + if (todayStart && todayEnd) { + sq.whereBetween(`${qb.alias}.startedAt`, [todayStart, todayEnd]); + sq.whereBetween(`time_slot.startedAt`, [todayStart, todayEnd]); + } + if (isNotEmpty(employeeIds)) { + sq.whereIn(`${qb.alias}.employeeId`, employeeIds); + sq.whereIn(`time_slot.employeeId`, employeeIds); + } + if (isNotEmpty(projectIds)) { + sq.whereIn(`${qb.alias}.projectId`, projectIds); + } + if (isNotEmpty(taskIds)) { + sq.whereIn(`${qb.alias}.taskId`, taskIds); + } + if (isNotEmpty(organizationTeamId)) { + sq.andWhere(`${qb.alias}.organizationTeamId`, organizationTeamId); + } + + sq.groupBy([`${qb.alias}.id`, 'task.id']); // Apply multiple group by clauses in a single statement + sq.orderBy(`${qb.alias}.updatedAt`, 'desc'); // Apply order by clause + console.log(chalk.green(sq.toString() + ' || Get Today Statistics Query MikroORM!')); + // Execute the raw SQL query and get the results + todayStatistics = (await knex.raw(sq.toString())).rows || []; + } break; - case DatabaseTypeEnum.mysql: - todayQueryString = p( - `COALESCE(ROUND(SUM(TIMESTAMPDIFF(SECOND, "${todayQuery.alias}"."startedAt", COALESCE("${todayQuery.alias}"."stoppedAt", NOW()))) / COUNT("time_slot"."id")), 0)` - ); + + case MultiORMEnum.TypeORM: { + const qb = this.typeOrmTimeLogRepository.createQueryBuilder('time_log'); + + qb.select(p(`"task"."title"`), 'title') + qb.addSelect(p(`"task"."id"`), 'taskId') + qb.addSelect(p(`"${qb.alias}"."updatedAt"`), 'updatedAt') + qb.addSelect(getTasksTodayDurationQueryString(dbType, qb.alias), `today_duration`) + + // Add join clauses + qb.innerJoin(`${qb.alias}.task`, 'task'); + qb.innerJoin(`${qb.alias}.timeSlots`, 'time_slot'); + + // Combine tenant and organization ID conditions + qb.andWhere(p(`"${qb.alias}"."tenantId" = :tenantId AND "${qb.alias}"."organizationId" = :organizationId`), { tenantId, organizationId }); + qb.andWhere(p(`"time_slot"."tenantId" = :tenantId AND "time_slot"."organizationId" = :organizationId`), { tenantId, organizationId }); + + // Add conditions based on today's start and end time + if (todayStart && todayEnd) { + qb.andWhere(p(`"${qb.alias}"."startedAt" BETWEEN :todayStart AND :todayEnd`), { todayStart, todayEnd }); + qb.andWhere(p(`"time_slot"."startedAt" BETWEEN :todayStart AND :todayEnd`), { todayStart, todayEnd }); + } + if (isNotEmpty(employeeIds)) { + qb.andWhere(p(`"${qb.alias}"."employeeId" IN (:...employeeIds)`), { employeeIds }); + qb.andWhere(p(`"time_slot"."employeeId" IN (:...employeeIds)`), { employeeIds }); + } + if (isNotEmpty(projectIds)) { + qb.andWhere(p(`"${qb.alias}"."projectId" IN (:...projectIds)`), { projectIds }); + } + if (isNotEmpty(taskIds)) { + qb.andWhere(p(`"${qb.alias}"."taskId" IN (:...taskIds)`), { taskIds }); + } + if (isNotEmpty(organizationTeamId)) { + qb.andWhere(p(`"${qb.alias}"."organizationTeamId" = :organizationTeamId`), { organizationTeamId }); + } + + qb.groupBy(p(`"${qb.alias}"."id"`)) + qb.addGroupBy(p(`"task"."id"`)) + qb.orderBy(p(`"${qb.alias}"."updatedAt"`), 'DESC'); + console.log(qb.getQuery(), ' || Get Today Statistics Query TypeORM'); + // Execute the SQL query and get the results + todayStatistics = await qb.getRawMany(); + } break; default: - throw Error( - `cannot create statistic query due to unsupported database type: ${this.configService.dbConnectionOptions.type}` - ); + throw new Error(`Cannot create statistic query due to unsupported database type: ${dbType}`); } - todayQuery - .select(p(`"task"."title"`), 'title') - .addSelect(p(`"task"."id"`), 'taskId') - .addSelect(p(`"${todayQuery.alias}"."updatedAt"`), 'updatedAt') - .addSelect(todayQueryString, `today_duration`) - .innerJoin(`${todayQuery.alias}.task`, 'task') - .innerJoin(`${todayQuery.alias}.timeSlots`, 'time_slot') - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - if (todayStart && todayEnd) { - qb.andWhere(p(`"${todayQuery.alias}"."startedAt" BETWEEN :todayStart AND :todayEnd`), { - todayStart, - todayEnd - }); - qb.andWhere(p(`"time_slot"."startedAt" BETWEEN :todayStart AND :todayEnd`), { - todayStart, - todayEnd - }); - } - }) - ) - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - qb.andWhere(p(`"${todayQuery.alias}"."tenantId" = :tenantId`), { tenantId }); - qb.andWhere(p(`"${todayQuery.alias}"."organizationId" = :organizationId`), { organizationId }); - }) - ) - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - qb.andWhere(p(`"time_slot"."tenantId" = :tenantId`), { tenantId }); - qb.andWhere(p(`"time_slot"."organizationId" = :organizationId`), { organizationId }); - }) - ) - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - if (isNotEmpty(employeeIds)) { - qb.andWhere(p(`"${todayQuery.alias}"."employeeId" IN (:...employeeIds)`), { - employeeIds - }); - qb.andWhere(p(`"time_slot"."employeeId" IN (:...employeeIds)`), { - employeeIds - }); - } - if (isNotEmpty(projectIds)) { - qb.andWhere(p(`"${todayQuery.alias}"."projectId" IN (:...projectIds)`), { - projectIds - }); - } - if (isNotEmpty(taskIds)) { - qb.andWhere(p(`"${todayQuery.alias}"."taskId" IN (:...taskIds)`), { - taskIds - }); - } - if (isNotEmpty(organizationTeamId)) { - qb.andWhere(p(`"${todayQuery.alias}"."organizationTeamId" = :organizationTeamId`), { - organizationTeamId - }); - } - }) - ) - .groupBy(p(`"${todayQuery.alias}"."id"`)) - .addGroupBy(p(`"task"."id"`)) - .orderBy(p(`"${todayQuery.alias}"."updatedAt"`), 'DESC'); + let statistics: any[] = []; - const todayStatistics = await todayQuery.getRawMany(); + /** + * Get Given Time Frame Task Statistics + */ + switch (this.ormType) { + case MultiORMEnum.MikroORM: { + // Start building the MikroORM query + const qb = this.mikroOrmTimeLogRepository.createQueryBuilder('time_log'); + const knex = this.mikroOrmTimeLogRepository.getKnex(); + + // Add the raw SQL snippet to the select + const raw = getTasksDurationQueryString(dbType, qb.alias); + + // Constructs SQL query to fetch task title, ID, last updated timestamp, and today's duration. + let sq = knex(qb.alias).select([ + `task.title AS title`, + `task.id AS taskId`, + `${qb.alias}.updatedAt AS updatedAt`, + knex.raw(`${raw} AS duration`), + ]); + + // Add join clauses + sq.innerJoin('task', `${qb.alias}.taskId`, 'task.id'); + sq.innerJoin('time_slot_time_logs', `${qb.alias}.id`, 'time_slot_time_logs.timeLogId'); + sq.innerJoin('time_slot', 'time_slot_time_logs.timeSlotId', 'time_slot.id'); + + // Add where clauses + sq.andWhere({ + [`${qb.alias}.tenantId`]: tenantId, + [`${qb.alias}.organizationId`]: organizationId, + [`time_slot.tenantId`]: tenantId, + [`time_slot.organizationId`]: organizationId + }); - const query = this.typeOrmTimeLogRepository.createQueryBuilder(); + if (start && end) { + sq.whereBetween(`${qb.alias}.startedAt`, [start, end]); + sq.whereBetween(`time_slot.startedAt`, [start, end]); + } + if (isNotEmpty(employeeIds)) { + sq.whereIn(`${qb.alias}.employeeId`, employeeIds); + sq.whereIn(`time_slot.employeeId`, employeeIds); + } + if (isNotEmpty(projectIds)) { + sq.whereIn(`${qb.alias}.projectId`, projectIds); + } + if (isNotEmpty(taskIds)) { + sq.whereIn(`${qb.alias}.taskId`, taskIds); + } + if (isNotEmpty(organizationTeamId)) { + sq.andWhere(`${qb.alias}.organizationTeamId`, organizationTeamId); + } - let queryString: string; - switch (this.configService.dbConnectionOptions.type) { - case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: - queryString = `COALESCE(ROUND(SUM((julianday(COALESCE("${query.alias}"."stoppedAt", datetime('now'))) - julianday("${query.alias}"."startedAt")) * 86400) / COUNT("time_slot"."id")), 0)`; - break; - case DatabaseTypeEnum.postgres: - queryString = `COALESCE(ROUND(SUM(extract(epoch from (COALESCE("${query.alias}"."stoppedAt", NOW()) - "${query.alias}"."startedAt"))) / COUNT("time_slot"."id")), 0)`; + sq.groupBy([`${qb.alias}.id`, 'task.id']); // Apply multiple group by clauses in a single statement + sq.orderBy(`${qb.alias}.updatedAt`, 'desc'); // Apply order by clause + console.log(chalk.green(sq.toString() + ' || Get Statistics Query MikroORM!')); + // Execute the raw SQL query and get the results + statistics = (await knex.raw(sq.toString())).rows || []; + } break; - case DatabaseTypeEnum.mysql: - queryString = p( - `COALESCE(ROUND(SUM(TIMESTAMPDIFF(SECOND, "${query.alias}"."startedAt", COALESCE("${query.alias}"."stoppedAt", NOW()))) / COUNT("time_slot"."id")), 0)` - ); + + case MultiORMEnum.TypeORM: { + /** + * Get Time Range Statistics + */ + const qb = this.typeOrmTimeLogRepository.createQueryBuilder('time_log'); + qb.select(p(`"task"."title"`), 'title') + qb.addSelect(p(`"task"."id"`), 'taskId') + qb.addSelect(p(`"${qb.alias}"."updatedAt"`), 'updatedAt') + qb.addSelect(getTasksDurationQueryString(dbType, qb.alias), `duration`) + + // Add join clauses + qb.innerJoin(`${qb.alias}.task`, 'task'); + qb.innerJoin(`${qb.alias}.timeSlots`, 'time_slot'); + + // Add join clauses + // Combine tenant and organization ID conditions for qb.alias + qb.andWhere(p(`"${qb.alias}"."tenantId" = :tenantId AND "${qb.alias}"."organizationId" = :organizationId`), { tenantId, organizationId }); + // Combine tenant and organization ID conditions for time_slot + qb.andWhere(p(`"time_slot"."tenantId" = :tenantId AND "time_slot"."organizationId" = :organizationId`), { tenantId, organizationId }); + + // Add conditions based on start and end time + if (start && end) { + qb.andWhere(p(`"${qb.alias}"."startedAt" BETWEEN :start AND :end`), { start, end }); + qb.andWhere(p(`"time_slot"."startedAt" BETWEEN :start AND :end`), { start, end }); + } + if (isNotEmpty(employeeIds)) { + qb.andWhere(p(`"${qb.alias}"."employeeId" IN (:...employeeIds) AND "time_slot"."employeeId" IN (:...employeeIds)`), { employeeIds }); + } + if (isNotEmpty(projectIds)) { + qb.andWhere(p(`"${qb.alias}"."projectId" IN (:...projectIds)`), { projectIds }); + } + if (isNotEmpty(taskIds)) { + qb.andWhere(p(`"${qb.alias}"."taskId" IN (:...taskIds)`), { taskIds }); + } + if (isNotEmpty(organizationTeamId)) { + qb.andWhere(p(`"${qb.alias}"."organizationTeamId" = :organizationTeamId`), { organizationTeamId }); + } + + qb.groupBy(p(`"${qb.alias}"."id"`)); + qb.addGroupBy(p(`"task"."id"`)); + qb.orderBy(p(`"${qb.alias}"."updatedAt"`), 'DESC'); + console.log(qb.getQueryAndParameters(), 'Get Statistics Query TypeORM'); + // Execute the raw SQL query and get the results + statistics = await qb.getRawMany(); + } break; default: - throw Error( - `cannot create statistic query due to unsupported database type: ${this.configService.dbConnectionOptions.type}` - ); + throw new Error(`Cannot create statistic query due to unsupported database type: ${dbType}`); } - query - .select(p(`"task"."title"`), 'title') - .addSelect(p(`"task"."id"`), 'taskId') - .addSelect(p(`"${query.alias}"."updatedAt"`), 'updatedAt') - .addSelect(queryString, `duration`) - .innerJoin(`${query.alias}.task`, 'task') - .innerJoin(`${query.alias}.timeSlots`, 'time_slot') - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - if (start && end) { - qb.andWhere(p(`"${query.alias}"."startedAt" BETWEEN :start AND :end`), { - start, - end - }); - qb.andWhere(p(`"time_slot"."startedAt" BETWEEN :start AND :end`), { - start, - end - }); - } - }) - ) - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - qb.andWhere(p(`"${query.alias}"."tenantId" = :tenantId`), { tenantId }); - qb.andWhere(p(`"${query.alias}"."organizationId" = :organizationId`), { organizationId }); - }) - ) - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - qb.andWhere(p(`"time_slot"."tenantId" = :tenantId`), { tenantId }); - qb.andWhere(p(`"time_slot"."organizationId" = :organizationId`), { organizationId }); - }) - ) - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - if (isNotEmpty(employeeIds)) { - qb.andWhere(p(`"${query.alias}"."employeeId" IN (:...employeeIds)`), { - employeeIds - }); - qb.andWhere(p(`"time_slot"."employeeId" IN (:...employeeIds)`), { - employeeIds - }); - } - if (isNotEmpty(projectIds)) { - qb.andWhere(p(`"${query.alias}"."projectId" IN (:...projectIds)`), { - projectIds - }); - } - if (isNotEmpty(taskIds)) { - qb.andWhere(p(`"${query.alias}"."taskId" IN (:...taskIds)`), { - taskIds - }); - } - if (isNotEmpty(organizationTeamId)) { - qb.andWhere(p(`"${query.alias}"."organizationTeamId" = :organizationTeamId`), { - organizationTeamId - }); - } - }) - ) - .groupBy(p(`"${query.alias}"."id"`)) - .addGroupBy(p(`"task"."id"`)) - .orderBy(p(`"${todayQuery.alias}"."updatedAt"`), 'DESC'); + let totalDuration: any; - const statistics = await query.getRawMany(); + /** + * Get Total Task Statistics + */ + switch (this.ormType) { + case MultiORMEnum.MikroORM: { + const qb = this.mikroOrmTimeLogRepository.createQueryBuilder('time_log'); + const knex = this.mikroOrmTimeLogRepository.getKnex(); + + // Add the raw SQL snippet to the select + const raw = getTasksTotalDurationQueryString(dbType, qb.alias); + // Construct your SQL query using knex + let sq = knex(qb.alias).select([ + knex.raw(`${raw} AS duration`) + ]); + + // Add join clauses + sq.innerJoin('task', `${qb.alias}.taskId`, 'task.id'); + sq.innerJoin('time_slot_time_logs', `${qb.alias}.id`, 'time_slot_time_logs.timeLogId'); + sq.innerJoin('time_slot', 'time_slot_time_logs.timeSlotId', 'time_slot.id'); + + // Add where clauses + sq.andWhere({ + [`${qb.alias}.tenantId`]: tenantId, + [`${qb.alias}.organizationId`]: organizationId + }); - const totalDurationQuery = this.typeOrmTimeLogRepository.createQueryBuilder(); + if (start && end) { + sq.whereBetween(`${qb.alias}.startedAt`, [start, end]); + } + if (isNotEmpty(employeeIds)) { + sq.whereIn(`${qb.alias}.employeeId`, employeeIds); + } + if (isNotEmpty(projectIds)) { + sq.whereIn(`${qb.alias}.projectId`, projectIds); + } + if (isNotEmpty(organizationTeamId)) { + sq.andWhere(`${qb.alias}.organizationTeamId`, organizationTeamId); + } + + console.log(chalk.green(sq.toString() + ' || Get Total Duration Query MikroORM!')); + // Execute the raw SQL query and get the results + [totalDuration] = (await knex.raw(sq.toString())).rows || []; + } - let totalDurationQueryString: string; - switch (this.configService.dbConnectionOptions.type) { - case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: - totalDurationQueryString = `COALESCE(ROUND(SUM((julianday(COALESCE("${totalDurationQuery.alias}"."stoppedAt", datetime('now'))) - julianday("${totalDurationQuery.alias}"."startedAt")) * 86400)), 0)`; - break; - case DatabaseTypeEnum.postgres: - totalDurationQueryString = `COALESCE(ROUND(SUM(extract(epoch from (COALESCE("${totalDurationQuery.alias}"."stoppedAt", NOW()) - "${totalDurationQuery.alias}"."startedAt")))), 0)`; break; - case DatabaseTypeEnum.mysql: - totalDurationQueryString = p( - `COALESCE(ROUND(SUM(TIMESTAMPDIFF(SECOND, "${totalDurationQuery.alias}"."startedAt", COALESCE("${totalDurationQuery.alias}"."stoppedAt", NOW())))), 0)` - ); + + case MultiORMEnum.TypeORM: { + const qb = this.typeOrmTimeLogRepository.createQueryBuilder('time_log'); + qb.select(getTasksTotalDurationQueryString(dbType, qb.alias), 'duration'); + + // Add join clauses + qb.innerJoin(`${qb.alias}.task`, 'task'); + + // Add where clauses + qb.andWhere(p(`"${qb.alias}"."tenantId" = :tenantId`), { tenantId }); + qb.andWhere(p(`"${qb.alias}"."organizationId" = :organizationId`), { organizationId }); + + if (start && end) { + qb.andWhere(p(`"${qb.alias}"."startedAt" BETWEEN :start AND :end`), { start, end }); + } + if (isNotEmpty(employeeIds)) { + qb.andWhere(p(`"${qb.alias}"."employeeId" IN (:...employeeIds)`), { employeeIds }); + } + if (isNotEmpty(projectIds)) { + qb.andWhere(p(`"${qb.alias}"."projectId" IN (:...projectIds)`), { projectIds }); + } + if (isNotEmpty(organizationTeamId)) { + qb.andWhere(p(`"${qb.alias}"."organizationTeamId" = :organizationTeamId`), { organizationTeamId }); + } + + console.log(qb.getQuery(), 'Get Total Duration Query TypeORM!'); + // Execute the raw SQL query and get the results + totalDuration = await qb.getRawOne(); + } break; + default: - throw Error( - `cannot create statistic query due to unsupported database type: ${this.configService.dbConnectionOptions.type}` - ); + throw new Error(`Cannot create statistic query due to unsupported database type: ${dbType}`); } - totalDurationQuery - .select(totalDurationQueryString, `duration`) - .innerJoin(`${totalDurationQuery.alias}.task`, 'task') - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - if (start && end) { - qb.andWhere(p(`"${totalDurationQuery.alias}"."startedAt" BETWEEN :start AND :end`), { - start, - end - }); - } - }) - ) - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - qb.andWhere(p(`"${totalDurationQuery.alias}"."tenantId" = :tenantId`), { tenantId }); - qb.andWhere(p(`"${totalDurationQuery.alias}"."organizationId" = :organizationId`), { - organizationId - }); - }) - ) - .andWhere( - new Brackets((qb: WhereExpressionBuilder) => { - if (isNotEmpty(employeeIds)) { - qb.andWhere(p(`"${totalDurationQuery.alias}"."employeeId" IN (:...employeeIds)`), { - employeeIds - }); - } - if (isNotEmpty(projectIds)) { - qb.andWhere(p(`"${totalDurationQuery.alias}"."projectId" IN (:...projectIds)`), { - projectIds - }); - } - if (isNotEmpty(organizationTeamId)) { - qb.andWhere(p(`"${totalDurationQuery.alias}"."organizationTeamId" = :organizationTeamId`), { - organizationTeamId - }); - } - }) - ); - - const totalDuration = await totalDurationQuery.getRawOne(); - // ------------------------------------------------ console.log('Find Statistics length: ', statistics.length); console.log('Find Today Statistics length: ', todayStatistics.length); - console.log('Find Total Duration: ', totalDuration.duration); + console.log('Find Total Duration: ', totalDuration?.duration); /* Code that cause issues... We try to optimize it using "hashing" approach etc @@ -1354,7 +1396,7 @@ export class StatisticService { tasks = tasks.splice(0, take); } - tasks = tasks.map((task: any) => { + tasks = tasks.map((task: any) => { task.durationPercentage = parseFloat( parseFloat((task.duration * 100) / totalDuration.duration + '').toFixed(2) ); @@ -1380,7 +1422,7 @@ export class StatisticService { const taskId = stat.taskId; if (!acc[taskId]) { - acc[taskId] = { duration: 0, todayDuration: 0, title: stat.title }; + acc[taskId] = { duration: 0, todayDuration: 0, title: stat.title, updatedAt: stat.updatedAt }; } // Convert stat.duration to a number before adding diff --git a/packages/core/src/time-tracking/time-log/time-log.entity.ts b/packages/core/src/time-tracking/time-log/time-log.entity.ts index d417d7088fb..3b8a4cf4729 100644 --- a/packages/core/src/time-tracking/time-log/time-log.entity.ts +++ b/packages/core/src/time-tracking/time-log/time-log.entity.ts @@ -254,7 +254,7 @@ export class TimeLog extends TenantOrganizationBaseEntity implements ITimeLog { * TimeSlot */ @ApiProperty({ type: () => TimeSlot, isArray: true }) - @MultiORMManyToMany(() => TimeSlot, (timeLogs) => timeLogs.timeLogs, { + @MultiORMManyToMany(() => TimeSlot, (it) => it.timeLogs, { /** Database cascade action on update. */ onUpdate: 'CASCADE', /** Database cascade action on delete. */ diff --git a/packages/core/src/time-tracking/time-slot/time-slot.entity.ts b/packages/core/src/time-tracking/time-slot/time-slot.entity.ts index 85a6a36a416..1061e16cabd 100644 --- a/packages/core/src/time-tracking/time-slot/time-slot.entity.ts +++ b/packages/core/src/time-tracking/time-slot/time-slot.entity.ts @@ -131,12 +131,18 @@ export class TimeSlot extends TenantOrganizationBaseEntity * TimeLog */ @MultiORMManyToMany(() => TimeLog, (it) => it.timeSlots, { + /** Database cascade action on update. */ onUpdate: 'CASCADE', + /** Database cascade action on delete. */ onDelete: 'CASCADE', + /** This column is a boolean flag indicating whether the current entity is the 'owning' side of a relationship. */ owner: true, + /** Pivot table for many-to-many relationship. */ pivotTable: 'time_slot_time_logs', + /** Column in pivot table referencing 'time_slot' primary key. */ joinColumn: 'timeSlotId', - inverseJoinColumn: 'timeLogId', + /** Column in pivot table referencing 'time_logs' primary key. */ + inverseJoinColumn: 'timeLogId' }) @JoinTable({ name: 'time_slot_time_logs' }) timeLogs?: ITimeLog[]; diff --git a/packages/core/src/time-tracking/time-slot/time-slot.module.ts b/packages/core/src/time-tracking/time-slot/time-slot.module.ts index 0f9e3162a57..3b7f103056a 100644 --- a/packages/core/src/time-tracking/time-slot/time-slot.module.ts +++ b/packages/core/src/time-tracking/time-slot/time-slot.module.ts @@ -11,11 +11,10 @@ import { TimeSlotService } from './time-slot.service'; import { TimeLogModule } from './../time-log/time-log.module'; import { EmployeeModule } from './../../employee/employee.module'; import { ActivityModule } from './../activity/activity.module'; +import { TypeOrmTimeSlotRepository } from './repository/type-orm-time-slot.repository'; @Module({ - controllers: [ - TimeSlotController - ], + controllers: [TimeSlotController], imports: [ TypeOrmModule.forFeature([TimeSlot, TimeSlotMinute]), MikroOrmModule.forFeature([TimeSlot, TimeSlotMinute]), @@ -25,14 +24,7 @@ import { ActivityModule } from './../activity/activity.module'; forwardRef(() => ActivityModule), CqrsModule ], - providers: [ - TimeSlotService, - ...CommandHandlers - ], - exports: [ - TimeSlotService, - TypeOrmModule, - MikroOrmModule - ] + providers: [TimeSlotService, TypeOrmTimeSlotRepository, ...CommandHandlers], + exports: [TypeOrmModule, MikroOrmModule, TimeSlotService, TypeOrmTimeSlotRepository] }) export class TimeSlotModule { } diff --git a/packages/core/src/upwork/upwork.service.ts b/packages/core/src/upwork/upwork.service.ts index 367d34f16a5..2ec4d2d81ce 100644 --- a/packages/core/src/upwork/upwork.service.ts +++ b/packages/core/src/upwork/upwork.service.ts @@ -22,8 +22,8 @@ import { RolesEnum, ExpenseCategoriesEnum, OrganizationVendorEnum, - IUpworkOfferStatusEnum, - IUpworkProposalStatusEnum, + // IUpworkOfferStatusEnum, + // IUpworkProposalStatusEnum, IUpworkDateRange, ContactType, TimeLogSourceEnum, @@ -67,12 +67,12 @@ import { IncomeCreateCommand } from '../income/commands'; import { ExpenseCreateCommand } from '../expense/commands'; import { OrganizationContactCreateCommand } from '../organization-contact/commands'; import { - UpworkJobService, - UpworkOffersService, + // UpworkJobService, + // UpworkOffersService, UpworkReportService } from '@gauzy/integration-upwork'; import { TimeLogCreateCommand } from '../time-tracking/time-log/commands'; -import { ProposalCreateCommand } from '../proposal/commands/proposal-create.command'; +// import { ProposalCreateCommand } from '../proposal/commands/proposal-create.command'; import { CreateTimeSlotMinutesCommand, TimeSlotCreateCommand @@ -95,8 +95,8 @@ export class UpworkService { private readonly _organizationService: OrganizationService, private readonly _timeSlotService: TimeSlotService, private readonly _upworkReportService: UpworkReportService, - private readonly _upworkJobService: UpworkJobService, - private readonly _upworkOfferService: UpworkOffersService, + // private readonly _upworkJobService: UpworkJobService, + // private readonly _upworkOfferService: UpworkOffersService, private readonly commandBus: CommandBus ) { } @@ -745,13 +745,13 @@ export class UpworkService { providerId, entity.datePicker.selectedDate ); - case 'proposal': - return await this.syncProposalsOffers( - organizationId, - integrationId, - config, - employeeId - ); + // case 'proposal': + // return await this.syncProposalsOffers( + // organizationId, + // integrationId, + // config, + // employeeId + // ); default: return; } @@ -1422,196 +1422,196 @@ export class UpworkService { /* * Sync upwork offers for freelancer */ - async syncProposalsOffers( - organizationId: string, - integrationId: string, - config: IUpworkApiConfig, - employeeId: string - ) { - const proposals = await this._getProposals(config); - const offers = await this._getOffers(config); - - const syncedOffers = await this._syncOffers( - config, - offers, - organizationId, - integrationId, - employeeId - ); - - const syncedProposals = await this._syncProposals(proposals); - return { - syncedOffers, - syncedProposals - }; - } + // async syncProposalsOffers( + // organizationId: string, + // integrationId: string, + // config: IUpworkApiConfig, + // employeeId: string + // ) { + // const proposals = await this._getProposals(config); + // const offers = await this._getOffers(config); + + // const syncedOffers = await this._syncOffers( + // config, + // offers, + // organizationId, + // integrationId, + // employeeId + // ); + + // const syncedProposals = await this._syncProposals(proposals); + // return { + // syncedOffers, + // syncedProposals + // }; + // } /* * Sync upwork proposals for freelancer */ - private async _getProposals(config: IUpworkApiConfig) { - try { - const promises = []; - for (const status in IUpworkProposalStatusEnum) { - if (isNaN(Number(status))) { - promises.push( - this._upworkOfferService - .getProposalLisByFreelancer( - config, - IUpworkProposalStatusEnum[status] - ) - .then((response) => response) - .catch((error) => error) - ); - } - } - return Promise.all(promises).then(async (results: any[]) => { - return results; - }); - } catch (error) { - throw new BadRequestException('Cannot sync proposals'); - } - } + // private async _getProposals(config: IUpworkApiConfig) { + // try { + // const promises = []; + // for (const status in IUpworkProposalStatusEnum) { + // if (isNaN(Number(status))) { + // promises.push( + // this._upworkOfferService + // .getProposalLisByFreelancer( + // config, + // IUpworkProposalStatusEnum[status] + // ) + // .then((response) => response) + // .catch((error) => error) + // ); + // } + // } + // return Promise.all(promises).then(async (results: any[]) => { + // return results; + // }); + // } catch (error) { + // throw new BadRequestException('Cannot sync proposals'); + // } + // } /* * Sync upwork offers for freelancer */ - private async _getOffers(config: IUpworkApiConfig) { - try { - const promises = []; - for (const status in IUpworkOfferStatusEnum) { - if (isNaN(Number(status))) { - promises.push( - this._upworkOfferService - .getOffersListByFreelancer( - config, - IUpworkOfferStatusEnum[status] - ) - .then((response) => response) - .catch((error) => error) - ); - } - } - return Promise.all(promises).then(async (results: any[]) => { - return results; - }); - } catch (error) { - throw new BadRequestException('Cannot sync offers'); - } - } + // private async _getOffers(config: IUpworkApiConfig) { + // try { + // const promises = []; + // for (const status in IUpworkOfferStatusEnum) { + // if (isNaN(Number(status))) { + // promises.push( + // this._upworkOfferService + // .getOffersListByFreelancer( + // config, + // IUpworkOfferStatusEnum[status] + // ) + // .then((response) => response) + // .catch((error) => error) + // ); + // } + // } + // return Promise.all(promises).then(async (results: any[]) => { + // return results; + // }); + // } catch (error) { + // throw new BadRequestException('Cannot sync offers'); + // } + // } /* * Sync upwork offers for freelancer */ - private async _syncOffers( - config: IUpworkApiConfig, - offers, - organizationId: string, - integrationId: string, - employeeId: string - ) { - return await Promise.all( - offers - .filter( - (row) => - row['offers'] && row['offers'].hasOwnProperty('offer') - ) - .map((row) => row['offers']) - .map(async (row) => { - const { offer: items } = row; - let integratedOffers = []; - - for await (const item of items) { - const { - title: proposalContent, - terms_data, - last_event_state, - job_posting_ref, - rid: sourceId - } = item; - let { title: jobPostContent } = item; - //find upwork job - const job = await this._upworkJobService - .getJobProfileByKey(config, job_posting_ref) - .then((response) => response) - .catch((error) => error); - - //if job not found/closed - if (job.statusCode !== 400) { - const { profile } = job; - jobPostContent = profile['op_description']; - } - - const tenantId = RequestContext.currentTenantId(); - const integrationMap = await this._integrationMapService.findOneOrFailByOptions( - { - where: { - sourceId, - entity: IntegrationEntity.PROPOSAL, - organizationId, - tenantId - } - } - ); - - let integratedOffer; - if ( - integrationMap && - integrationMap['success'] === true - ) { - integratedOffer = integrationMap.record; - } else { - const gauzyOffer = await this.commandBus.execute( - new ProposalCreateCommand({ - employeeId, - organizationId, - valueDate: new Date( - unixTimestampToDate( - terms_data.start_date - ) - ), - status: last_event_state - .trim() - .toUpperCase(), - proposalContent, - jobPostContent, - jobPostUrl: job_posting_ref - }) - ); - - integratedOffer = await this.commandBus.execute( - new IntegrationMapSyncEntityCommand({ - gauzyId: gauzyOffer.id, - integrationId, - sourceId, - entity: IntegrationEntity.PROPOSAL, - organizationId - }) - ); - } - - integratedOffers = integratedOffers.concat( - integratedOffer - ); - } - return integratedOffers; - }) - ); - } + // private async _syncOffers( + // config: IUpworkApiConfig, + // offers, + // organizationId: string, + // integrationId: string, + // employeeId: string + // ) { + // return await Promise.all( + // offers + // .filter( + // (row) => + // row['offers'] && row['offers'].hasOwnProperty('offer') + // ) + // .map((row) => row['offers']) + // .map(async (row) => { + // const { offer: items } = row; + // let integratedOffers = []; + + // for await (const item of items) { + // const { + // title: proposalContent, + // terms_data, + // last_event_state, + // job_posting_ref, + // rid: sourceId + // } = item; + // let { title: jobPostContent } = item; + // //find upwork job + // const job = await this._upworkJobService + // .getJobProfileByKey(config, job_posting_ref) + // .then((response) => response) + // .catch((error) => error); + + // //if job not found/closed + // if (job.statusCode !== 400) { + // const { profile } = job; + // jobPostContent = profile['op_description']; + // } + + // const tenantId = RequestContext.currentTenantId(); + // const integrationMap = await this._integrationMapService.findOneOrFailByOptions( + // { + // where: { + // sourceId, + // entity: IntegrationEntity.PROPOSAL, + // organizationId, + // tenantId + // } + // } + // ); + + // let integratedOffer; + // if ( + // integrationMap && + // integrationMap['success'] === true + // ) { + // integratedOffer = integrationMap.record; + // } else { + // const gauzyOffer = await this.commandBus.execute( + // new ProposalCreateCommand({ + // employeeId, + // organizationId, + // valueDate: new Date( + // unixTimestampToDate( + // terms_data.start_date + // ) + // ), + // status: last_event_state + // .trim() + // .toUpperCase(), + // proposalContent, + // jobPostContent, + // jobPostUrl: job_posting_ref + // }) + // ); + + // integratedOffer = await this.commandBus.execute( + // new IntegrationMapSyncEntityCommand({ + // gauzyId: gauzyOffer.id, + // integrationId, + // sourceId, + // entity: IntegrationEntity.PROPOSAL, + // organizationId + // }) + // ); + // } + + // integratedOffers = integratedOffers.concat( + // integratedOffer + // ); + // } + // return integratedOffers; + // }) + // ); + // } /* * Sync upwork proposals for freelancer */ - private async _syncProposals(proposals) { - return await Promise.all( - proposals - .filter( - (row) => - row['data'] && - row['data'].hasOwnProperty('applications') - ) - .map((row) => row.data.applications) - .map(async (row) => row) - ); - } + // private async _syncProposals(proposals) { + // return await Promise.all( + // proposals + // .filter( + // (row) => + // row['data'] && + // row['data'].hasOwnProperty('applications') + // ) + // .map((row) => row.data.applications) + // .map(async (row) => row) + // ); + // } } diff --git a/packages/core/src/user-organization/dto/find-me-user-organization.dto.ts b/packages/core/src/user-organization/dto/find-me-user-organization.dto.ts new file mode 100644 index 00000000000..317ac419d91 --- /dev/null +++ b/packages/core/src/user-organization/dto/find-me-user-organization.dto.ts @@ -0,0 +1,10 @@ +import { IntersectionType } from "@nestjs/swagger"; +import { PickType } from "@nestjs/mapped-types"; +import { FindMeQueryDTO } from "../../user/dto"; + +/** + * DTO (Data Transfer Object) for finding user organization with "Find Me" query parameters. + */ +export class FindMeUserOrganizationDTO extends IntersectionType( + PickType(FindMeQueryDTO, ['includeEmployee'] as const), +) { } diff --git a/packages/core/src/user-organization/user-organization.controller.ts b/packages/core/src/user-organization/user-organization.controller.ts index 5768010bec4..f3cb693f850 100644 --- a/packages/core/src/user-organization/user-organization.controller.ts +++ b/packages/core/src/user-organization/user-organization.controller.ts @@ -1,16 +1,17 @@ import { Controller, HttpStatus, Get, Query, UseGuards, HttpCode, Delete, Param } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { CrudController } from './../core/crud'; -import { IUserOrganization, RolesEnum, LanguagesEnum, IPagination, IUser } from '@gauzy/contracts'; -import { UserOrganizationService } from './user-organization.services'; -import { UserOrganization } from './user-organization.entity'; import { Not } from 'typeorm'; -import { CommandBus } from '@nestjs/cqrs'; -import { UserOrganizationDeleteCommand } from './commands'; import { I18nLang } from 'nestjs-i18n'; -import { ParseJsonPipe, UUIDValidationPipe } from './../shared/pipes'; +import { IUserOrganization, RolesEnum, LanguagesEnum, IPagination, IUser } from '@gauzy/contracts'; +import { CrudController, PaginationParams } from './../core/crud'; +import { UUIDValidationPipe } from './../shared/pipes'; import { TenantPermissionGuard } from './../shared/guards'; import { UserDecorator } from './../shared/decorators'; +import { UserOrganizationService } from './user-organization.services'; +import { UserOrganization } from './user-organization.entity'; +import { UserOrganizationDeleteCommand } from './commands'; +import { FindMeUserOrganizationDTO } from './dto/find-me-user-organization.dto'; @ApiTags('UserOrganization') @UseGuards(TenantPermissionGuard) @@ -23,6 +24,11 @@ export class UserOrganizationController extends CrudController super(userOrganizationService); } + /** + * + * @param params + * @returns + */ @ApiOperation({ summary: 'Find all UserOrganizations.' }) @ApiResponse({ status: HttpStatus.OK, @@ -34,12 +40,14 @@ export class UserOrganizationController extends CrudController description: 'Record not found' }) @Get() - async findAll(@Query('data', ParseJsonPipe) data: any): Promise> { - const { relations, findInput } = data; - return this.userOrganizationService.findAll({ - where: findInput, - relations - }); + async findAll( + @Query() params: PaginationParams, + @Query() query: FindMeUserOrganizationDTO, + ): Promise> { + return await this.userOrganizationService.findAllUserOrganizations( + params, + query.includeEmployee + ); } @ApiOperation({ summary: 'Delete user from organization' }) diff --git a/packages/core/src/user-organization/user-organization.module.ts b/packages/core/src/user-organization/user-organization.module.ts index dd97b7ea68a..1f85fc49cbd 100644 --- a/packages/core/src/user-organization/user-organization.module.ts +++ b/packages/core/src/user-organization/user-organization.module.ts @@ -7,6 +7,7 @@ import { TenantModule } from '../tenant/tenant.module'; import { RolePermissionModule } from '../role-permission/role-permission.module'; import { OrganizationModule } from './../organization/organization.module'; import { UserModule } from './../user/user.module'; +import { EmployeeModule } from '../employee/employee.module'; import { RoleModule } from './../role/role.module'; import { UserOrganizationService } from './user-organization.services'; import { UserOrganizationController } from './user-organization.controller'; @@ -25,6 +26,7 @@ import { CommandHandlers } from './commands/handlers'; forwardRef(() => RolePermissionModule), forwardRef(() => OrganizationModule), forwardRef(() => UserModule), + forwardRef(() => EmployeeModule), forwardRef(() => RoleModule), CqrsModule ], diff --git a/packages/core/src/user-organization/user-organization.services.ts b/packages/core/src/user-organization/user-organization.services.ts index 9f5bbdb7788..b841283e844 100644 --- a/packages/core/src/user-organization/user-organization.services.ts +++ b/packages/core/src/user-organization/user-organization.services.ts @@ -1,26 +1,69 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { IOrganization, ITenant, IUser, IUserOrganization, RolesEnum } from '@gauzy/contracts'; -import { TenantAwareCrudService } from './../core/crud'; -import { Organization } from './../core/entities/internal'; +import { IOrganization, IPagination, ITenant, IUser, IUserOrganization, RolesEnum } from '@gauzy/contracts'; +import { PaginationParams, TenantAwareCrudService } from './../core/crud'; +import { Employee, Organization } from './../core/entities/internal'; import { TypeOrmOrganizationRepository } from '../organization/repository'; import { UserOrganization } from './user-organization.entity'; import { MikroOrmUserOrganizationRepository, TypeOrmUserOrganizationRepository } from './repository'; +import { EmployeeService } from '../employee/employee.service'; @Injectable() export class UserOrganizationService extends TenantAwareCrudService { constructor( - @InjectRepository(UserOrganization) - readonly typeOrmUserOrganizationRepository: TypeOrmUserOrganizationRepository, - + @InjectRepository(UserOrganization) readonly typeOrmUserOrganizationRepository: TypeOrmUserOrganizationRepository, readonly mikroOrmUserOrganizationRepository: MikroOrmUserOrganizationRepository, - - @InjectRepository(Organization) - readonly typeOrmOrganizationRepository: TypeOrmOrganizationRepository + @InjectRepository(Organization) readonly typeOrmOrganizationRepository: TypeOrmOrganizationRepository, + private readonly employeeService: EmployeeService, ) { super(typeOrmUserOrganizationRepository, mikroOrmUserOrganizationRepository); } + /** + * Finds all user organizations based on the provided filter options. + * + * @param filter Optional filter options to apply when querying user organizations. + * @returns A promise resolving to an array of user organizations. + */ + async findAllUserOrganizations( + filter: PaginationParams, + includeEmployee: boolean + ): Promise> { + // Call the base class method to find all user organizations + const { items, total } = await super.findAll(filter); + + // If 'includeEmployee' is set to true, fetch employee details associated with each user organization + if (includeEmployee) { + try { + // Extract user IDs from the items array + const userIds = items.map(organization => organization.user.id); + + // Fetch all employee details in bulk for the extracted user IDs + const employees = await this.employeeService.findEmployeesByUserIds(userIds); + + // Map employee details to a dictionary for easier lookup + const employeeMap = new Map(); + employees.forEach((employee) => { + employeeMap.set(employee.userId, employee); + }); + + // Merge employee details into each user organization object + const itemsWithEmployees = items.map(organization => { + const employee = employeeMap.get(organization.user.id); + return { ...organization, user: { ...organization.user, employee } }; + }); + + // Return paginated result with employee details + return { items: itemsWithEmployees, total }; + } catch (error) { + console.error(`Error fetching employee details: ${error.message}`); + } + } + + // Return original items if 'includeEmployee' is false + return { items, total }; + } + /** * Adds a user to all organizations within a specific tenant. * diff --git a/packages/core/src/user/factory-reset/factory-reset.service.ts b/packages/core/src/user/factory-reset/factory-reset.service.ts index c6282846ccd..dc903e6a83d 100644 --- a/packages/core/src/user/factory-reset/factory-reset.service.ts +++ b/packages/core/src/user/factory-reset/factory-reset.service.ts @@ -31,7 +31,6 @@ import { EmployeeAppointment, EmployeeAward, EmployeeLevel, - EmployeeProposalTemplate, EmployeeRecurringExpense, EmployeeSetting, Equipment, @@ -56,9 +55,6 @@ import { Invoice, InvoiceEstimateHistory, InvoiceItem, - JobPreset, - JobSearchCategory, - JobSearchOccupation, KeyResult, KeyResultTemplate, KeyResultUpdate, @@ -85,7 +81,6 @@ import { ProductVariant, ProductVariantPrice, ProductVariantSetting, - Proposal, RequestApproval, Screenshot, Skill, @@ -140,16 +135,8 @@ import { MikroOrmEmployeeAppointmentRepository } from '../../employee-appointmen import { TypeOrmEmployeeAppointmentRepository } from '../../employee-appointment/repository/type-orm-employee-appointment.repository'; import { MikroOrmEmployeeAwardRepository } from '../../employee-award/repository/mikro-orm-employee-award.repository'; import { TypeOrmEmployeeAwardRepository } from '../../employee-award/repository/type-orm-employee-award.repository'; -import { MikroOrmJobSearchCategoryRepository } from '../../employee-job-preset/job-search-category/repository/mikro-orm-job-search-category.repository'; -import { TypeOrmJobSearchCategoryRepository } from '../../employee-job-preset/job-search-category/repository/type-orm-job-search-category.repository'; -import { MikroOrmJobSearchOccupationRepository } from '../../employee-job-preset/job-search-occupation/repository/mikro-orm-job-search-occupation.repository'; -import { TypeOrmJobSearchOccupationRepository } from '../../employee-job-preset/job-search-occupation/repository/type-orm-job-search-occupation.repository'; -import { MikroOrmJobPresetRepository } from '../../employee-job-preset/repository/mikro-orm-job-preset.repository'; -import { TypeOrmJobPresetRepository } from '../../employee-job-preset/repository/type-orm-job-preset.repository'; import { MikroOrmEmployeeLevelRepository } from '../../employee-level/repository/mikro-orm-employee-level.repository'; import { TypeOrmEmployeeLevelRepository } from '../../employee-level/repository/type-orm-employee-level.repository'; -import { MikroOrmEmployeeProposalTemplateRepository } from '../../employee-proposal-template/repository/mikro-orm-employee-proposal-template.repository'; -import { TypeOrmEmployeeProposalTemplateRepository } from '../../employee-proposal-template/repository/type-orm-employee-proposal-template.repository'; import { MikroOrmEmployeeRecurringExpenseRepository } from '../../employee-recurring-expense/repository/mikro-orm-employee-recurring-expense.repository'; import { TypeOrmEmployeeRecurringExpenseRepository } from '../../employee-recurring-expense/repository/type-orm-employee-recurring-expense.repository'; import { MikroOrmEmployeeSettingRepository } from '../../employee-setting/repository/mikro-orm-employee-setting.repository'; @@ -252,8 +239,6 @@ import { MikroOrmProductVariantRepository } from '../../product-variant/reposito import { TypeOrmProductVariantRepository } from '../../product-variant/repository/type-orm-product-variant.repository'; import { MikroOrmProductRepository } from '../../product/repository/mikro-orm-product.repository'; import { TypeOrmProductRepository } from '../../product/repository/type-orm-product.repository'; -import { MikroOrmProposalRepository } from '../../proposal/repository/mikro-orm-proposal.repository'; -import { TypeOrmProposalRepository } from '../../proposal/repository/type-orm-proposal.repository'; import { MikroOrmRequestApprovalRepository } from '../../request-approval/repository/mikro-orm-request-approval.repository'; import { TypeOrmRequestApprovalRepository } from '../../request-approval/repository/type-orm-request-approval.repository'; import { MikroOrmSkillRepository } from '../../skills/repository/mikro-orm-skill.repository'; @@ -391,11 +376,6 @@ export class FactoryResetService { mikroOrmEmployeeAwardRepository: MikroOrmEmployeeAwardRepository, - @InjectRepository(EmployeeProposalTemplate) - private typeOrmEmployeeProposalTemplateRepository: TypeOrmEmployeeProposalTemplateRepository, - - mikroOrmEmployeeProposalTemplateRepository: MikroOrmEmployeeProposalTemplateRepository, - @InjectRepository(EmployeeRecurringExpense) private typeOrmEmployeeRecurringExpenseRepository: TypeOrmEmployeeRecurringExpenseRepository, @@ -516,21 +496,6 @@ export class FactoryResetService { mikroOrmInvoiceItemRepository: MikroOrmInvoiceItemRepository, - @InjectRepository(JobPreset) - private typeOrmJobPresetRepository: TypeOrmJobPresetRepository, - - mikroOrmJobPresetRepository: MikroOrmJobPresetRepository, - - @InjectRepository(JobSearchCategory) - private typeOrmJobSearchCategoryRepository: TypeOrmJobSearchCategoryRepository, - - mikroOrmJobSearchCategoryRepository: MikroOrmJobSearchCategoryRepository, - - @InjectRepository(JobSearchOccupation) - private typeOrmJobSearchOccupationRepository: TypeOrmJobSearchOccupationRepository, - - mikroOrmJobSearchOccupationRepository: MikroOrmJobSearchOccupationRepository, - @InjectRepository(KeyResult) private typeOrmKeyResultRepository: TypeOrmKeyResultRepository, @@ -666,11 +631,6 @@ export class FactoryResetService { mikroOrmProductVariantPriceRepository: MikroOrmProductVariantPriceRepository, - @InjectRepository(Proposal) - private typeOrmProposalRepository: TypeOrmProposalRepository, - - mikroOrmProposalRepository: MikroOrmProposalRepository, - @InjectRepository(Skill) private typeOrmSkillRepository: TypeOrmSkillRepository, @@ -737,7 +697,7 @@ export class FactoryResetService { mikroOrmUserOrganizationRepository: MikroOrmUserOrganizationRepository, private configService: ConfigService - ) {} + ) { } async onModuleInit() { this.registerCoreRepositories(); @@ -881,13 +841,9 @@ export class FactoryResetService { this.typeOrmInvoiceEstimateHistoryRepository, this.typeOrmInvoiceRepository, this.typeOrmFeatureOrganizationRepository, - this.typeOrmJobPresetRepository, - this.typeOrmJobSearchCategoryRepository, - this.typeOrmJobSearchOccupationRepository, this.typeOrmEmployeeAppointmentRepository, this.typeOrmEmployeeAwardRepository, this.typeOrmEmployeeLevelRepository, - this.typeOrmEmployeeProposalTemplateRepository, this.typeOrmEmployeeRecurringExpenseRepository, this.typeOrmEmployeeRepository, this.typeOrmEmployeeSettingRepository, @@ -925,7 +881,6 @@ export class FactoryResetService { this.typeOrmProductVariantSettingRepository, this.typeOrmPaymentRepository, this.typeOrmPipelineRepository, - this.typeOrmProposalRepository, this.typeOrmRequestApprovalRepository, this.typeOrmScreenshotRepository, this.typeOrmSkillRepository, diff --git a/packages/core/src/user/user.seed.ts b/packages/core/src/user/user.seed.ts index 60d772730ab..8888f24907d 100644 --- a/packages/core/src/user/user.seed.ts +++ b/packages/core/src/user/user.seed.ts @@ -16,6 +16,7 @@ import { IUser, ComponentLayoutStyleEnum } from '@gauzy/contracts'; +import { getEmailWithPostfix } from '../core/seeds/utils'; import { User } from './user.entity'; import { getUserDummyImage, Role } from '../core'; import { DEFAULT_EMPLOYEES, DEFAULT_EVER_EMPLOYEES } from '../employee/default-employees'; @@ -390,14 +391,14 @@ const generateRandomUser = async ( const firstName = faker.person.firstName(); const lastName = faker.person.lastName(); const username = faker.internet.userName(firstName, lastName); - const email = faker.internet.exampleEmail(firstName, lastName); + const email = getEmailWithPostfix(faker.internet.exampleEmail(firstName, lastName)); const avatar = faker.image.avatar(); const user = new User(); user.firstName = firstName; user.lastName = lastName; user.username = username; - user.email = email.toLowerCase(); + user.email = email; user.role = role; user.imageUrl = avatar; user.tenant = tenant; diff --git a/packages/core/src/warehouse/warehouse.seed.ts b/packages/core/src/warehouse/warehouse.seed.ts index 27dc6d148b3..0918122d00b 100644 --- a/packages/core/src/warehouse/warehouse.seed.ts +++ b/packages/core/src/warehouse/warehouse.seed.ts @@ -9,7 +9,8 @@ import { WarehouseProduct, WarehouseProductVariant, ImageAsset -} from './../core/entities/internal'; +} from '../core/entities/internal'; +import { getEmailWithPostfix } from '../core/seeds/utils'; export const createRandomWarehouses = async ( @@ -83,7 +84,7 @@ const applyRandomProperties = ( const warehouse = new Warehouse() warehouse.name = faker.company.name(); warehouse.code = faker.string.uuid(); - warehouse.email = faker.internet.exampleEmail(warehouse.name); + warehouse.email = getEmailWithPostfix(faker.internet.exampleEmail(warehouse.name)); warehouse.description = faker.lorem.words(); warehouse.active = faker.datatype.boolean(); warehouse.organization = organization; diff --git a/packages/desktop-api/package.json b/packages/desktop-api/package.json index bda3997272c..8b6a66a0a64 100644 --- a/packages/desktop-api/package.json +++ b/packages/desktop-api/package.json @@ -27,9 +27,9 @@ "keywords": [], "dependencies": { "@grpc/grpc-js": "^1.6.7", - "@mikro-orm/core": "^6.1.12", + "@mikro-orm/core": "^6.2.1", "@mikro-orm/nestjs": "^5.2.3", - "@mikro-orm/sqlite": "^6.1.12", + "@mikro-orm/sqlite": "^6.2.1", "@nestjs/common": "^10.3.7", "@nestjs/core": "^10.3.7", "@nestjs/typeorm": "^10.0.2", diff --git a/packages/desktop-libs/src/lib/config/read-write-file.ts b/packages/desktop-libs/src/lib/config/read-write-file.ts index ff175c96296..9cbf870dbc2 100644 --- a/packages/desktop-libs/src/lib/config/read-write-file.ts +++ b/packages/desktop-libs/src/lib/config/read-write-file.ts @@ -13,11 +13,13 @@ export class ReadWriteFile implements IReadWriteFile { return; } try { + console.log(`Reading file ${this._path.gauzyUi}`); return readFileSync(this._path.gauzyUi, 'utf8'); } catch (e) { console.error('Cannot read file'); } } + public get hasDirectoryAccess(): boolean { try { accessSync(this._path.dir, constants.W_OK); @@ -27,11 +29,14 @@ export class ReadWriteFile implements IReadWriteFile { return false; } } + public write(fileContent: string): void { if (!this.hasDirectoryAccess) { return; } + try { + console.log(`Writing file ${this._path.gauzyUi}`); writeFileSync(this._path.gauzyUi, fileContent); } catch (error) { console.log('Cannot change html file', error); diff --git a/packages/desktop-libs/src/lib/config/server-config.ts b/packages/desktop-libs/src/lib/config/server-config.ts index 7c8883970f9..6fa4ffcddd2 100644 --- a/packages/desktop-libs/src/lib/config/server-config.ts +++ b/packages/desktop-libs/src/lib/config/server-config.ts @@ -10,14 +10,23 @@ export class ServerConfig implements IServerConfig { public update(): void { if (!this._readWriteFile) return; - // read original file - let fileContent = this._readWriteFile.read(); - // replace all url in the file to normalize url file. - fileContent = this._replaceUrl(fileContent, this.apiUrl); - // remove duplicated content - fileContent = this._removeDuplicates(fileContent); - // override the original file - this._readWriteFile.write(fileContent); + + try { + // read original file + let fileContent = this._readWriteFile.read(); + + // replace all url in the file to normalize url file. + fileContent = this._replaceUrl(fileContent, this.apiUrl); + + // remove duplicated content + fileContent = this._removeDuplicates(fileContent); + + // override the original file + this._readWriteFile.write(fileContent); + } catch (error) { + console.error('Cannot update initial Server file', error); + throw new Error(error); + } } private _replaceUrl(fileContent: string, newUrl: string): string { diff --git a/packages/plugin/src/plugin.interface.ts b/packages/plugin/src/plugin.interface.ts index e3951071de6..e5595576e97 100644 --- a/packages/plugin/src/plugin.interface.ts +++ b/packages/plugin/src/plugin.interface.ts @@ -49,40 +49,30 @@ export interface IOnPluginDestroy { } /** - * Interface for plugins supporting basic seed operations. + * Interface for plugins supporting various seed operations. */ -export interface IOnPluginWithBasicSeed { +export interface IOnPluginSeedable { /** * Invoked when seeding basic plugin data. * @returns A void or a Promise representing the completion of the operation. */ - onPluginBasicSeed(): void | Promise; -} + onPluginBasicSeed?(): void | Promise; -/** - * Interface for plugins supporting default seed operations. - */ -export interface IOnPluginWithDefaultSeed { /** * Invoked when seeding default plugin data. * @returns A void or a Promise representing the completion of the operation. */ - onPluginDefaultSeed(): void | Promise; -} + onPluginDefaultSeed?(): void | Promise; -/** - * Interface for plugins supporting random seed operations. - */ -export interface IOnPluginWithRandomSeed { /** * Invoked when seeding random plugin data. * @returns A void or a Promise representing the completion of the operation. */ - onPluginRandomSeed(): void | Promise; + onPluginRandomSeed?(): void | Promise; } /** * Represents the combined lifecycle methods for a plugin. * This type combines interfaces for initializing and destroying a plugin. */ -export type PluginLifecycleMethods = IOnPluginBootstrap & IOnPluginDestroy & IOnPluginWithBasicSeed & IOnPluginWithDefaultSeed & IOnPluginWithRandomSeed; +export type PluginLifecycleMethods = IOnPluginBootstrap & IOnPluginDestroy & IOnPluginSeedable; diff --git a/packages/plugins/changelog/src/changelog.plugin.ts b/packages/plugins/changelog/src/changelog.plugin.ts index cc9f6106644..d510ba5b8bc 100644 --- a/packages/plugins/changelog/src/changelog.plugin.ts +++ b/packages/plugins/changelog/src/changelog.plugin.ts @@ -1,6 +1,6 @@ import * as chalk from 'chalk'; import { SeederModule } from '@gauzy/core'; -import { GauzyCorePlugin, IOnPluginBootstrap, IOnPluginDestroy, IOnPluginWithBasicSeed } from '@gauzy/plugin'; +import { GauzyCorePlugin, IOnPluginBootstrap, IOnPluginDestroy, IOnPluginSeedable } from '@gauzy/plugin'; import { ChangelogModule } from './changelog.module'; import { Changelog } from './changelog.entity'; import { ChangelogSeederService } from './changelog-seeder.service'; @@ -10,7 +10,7 @@ import { ChangelogSeederService } from './changelog-seeder.service'; entities: [Changelog], providers: [ChangelogSeederService] }) -export class ChangelogPlugin implements IOnPluginBootstrap, IOnPluginDestroy, IOnPluginWithBasicSeed { +export class ChangelogPlugin implements IOnPluginBootstrap, IOnPluginDestroy, IOnPluginSeedable { // We disable by default additional logging for each event to avoid cluttering the logs private logEnabled = true; diff --git a/packages/plugins/integration-wakatime/package.json b/packages/plugins/integration-wakatime/package.json index 5de5f27d589..61b3ea2919c 100644 --- a/packages/plugins/integration-wakatime/package.json +++ b/packages/plugins/integration-wakatime/package.json @@ -30,7 +30,7 @@ "dependencies": { "@gauzy/contracts": "^0.1.0", "@gauzy/core": "^0.1.0", - "@mikro-orm/core": "^6.1.12", + "@mikro-orm/core": "^6.2.1", "@mikro-orm/nestjs": "^5.2.3", "@nestjs/common": "^10.3.7", "@nestjs/typeorm": "^10.0.2", diff --git a/packages/plugins/job-proposal/.dockerignore b/packages/plugins/job-proposal/.dockerignore new file mode 100644 index 00000000000..b88b16df035 --- /dev/null +++ b/packages/plugins/job-proposal/.dockerignore @@ -0,0 +1,25 @@ +# Git +.git +.gitmodules +.gitignore + +# README file containing information about the project +README.md + +# Directory containing Docker-related files and configurations +docker + +# Directory containing installed Node.js modules, managed by npm or yarn +node_modules + +# Directory for temporary files +tmp + +# Directory for build-related files +build + +# Directory for distribution files, typically production-ready code +dist + +# Environment variables configuration file (dotenv file) +.env diff --git a/packages/plugins/job-proposal/README.md b/packages/plugins/job-proposal/README.md new file mode 100644 index 00000000000..c9eeb1b4c6d --- /dev/null +++ b/packages/plugins/job-proposal/README.md @@ -0,0 +1,15 @@ +# Job Proposal Plugin + +## Overview + +## Features + +## Installation + +To install the Job Proposal Plugin, simply run the following command in your terminal: + +```bash +npm install @gauzy/job-proposal-plugin +# or +yarn add @gauzy/job-proposal-plugin +``` diff --git a/packages/plugins/job-proposal/jest.config.js b/packages/plugins/job-proposal/jest.config.js new file mode 100644 index 00000000000..45dfed51a05 --- /dev/null +++ b/packages/plugins/job-proposal/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + name: 'job-proposal', + preset: '../../../jest.config.js', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + coverageDirectory: '../../coverage/plugins/job-proposal' +}; diff --git a/packages/plugins/job-proposal/package.json b/packages/plugins/job-proposal/package.json new file mode 100644 index 00000000000..d26a8aa58f7 --- /dev/null +++ b/packages/plugins/job-proposal/package.json @@ -0,0 +1,49 @@ +{ + "name": "@gauzy/job-proposal-plugin", + "version": "0.1.0", + "description": "Ever Gauzy Platform Job Proposal Plugin", + "author": { + "name": "Ever Co. LTD", + "email": "ever@ever.co", + "url": "https://ever.co" + }, + "license": "AGPL-3.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "directories": { + "lib": "dist", + "test": "__test__" + }, + "publishConfig": { + "access": "restricted" + }, + "scripts": { + "test:e2e": "jest --config ./jest.config.js", + "build": "rimraf dist && yarn run compile", + "compile": "tsc -p tsconfig.build.json" + }, + "keywords": [], + "dependencies": { + "@faker-js/faker": "8.0.0-alpha.0", + "@gauzy/contracts": "^0.1.0", + "@gauzy/core": "^0.1.0", + "@gauzy/plugin": "^0.1.0", + "@mikro-orm/nestjs": "^5.2.3", + "@nestjs/common": "^10.3.7", + "@nestjs/mapped-types": "^2.0.5", + "@nestjs/swagger": "^7.3.0", + "@nestjs/typeorm": "^10.0.2", + "class-validator": "^0.14.0", + "moment": "^2.29.4", + "typeorm": "^0.3.20" + }, + "devDependencies": { + "@types/node": "^17.0.33", + "rimraf": "^3.0.2", + "typescript": "5.1.6" + } +} diff --git a/packages/plugins/job-proposal/src/index.ts b/packages/plugins/job-proposal/src/index.ts new file mode 100644 index 00000000000..8c3c161ed36 --- /dev/null +++ b/packages/plugins/job-proposal/src/index.ts @@ -0,0 +1 @@ +export * from './job-proposal.plugin'; diff --git a/packages/plugins/job-proposal/src/job-proposal.plugin.ts b/packages/plugins/job-proposal/src/job-proposal.plugin.ts new file mode 100644 index 00000000000..51c163e1801 --- /dev/null +++ b/packages/plugins/job-proposal/src/job-proposal.plugin.ts @@ -0,0 +1,80 @@ +import * as chalk from 'chalk'; +import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy, IOnPluginSeedable } from '@gauzy/plugin'; +import { ApplicationPluginConfig } from '@gauzy/common'; +import { Proposal } from './proposal/proposal.entity'; +import { ProposalModule } from './proposal/proposal.moule'; +import { EmployeeProposalTemplateModule } from './proposal-template/employee-proposal-template.module'; +import { EmployeeProposalTemplate } from './proposal-template/employee-proposal-template.entity'; +import { ProposalSeederService } from './proposal/proposal-seeder.service'; + +@Plugin({ + imports: [ProposalModule, EmployeeProposalTemplateModule], + entities: [Proposal, EmployeeProposalTemplate], + configuration: (config: ApplicationPluginConfig) => { + config.customFields.Tag.push({ + propertyPath: 'proposals', + type: 'relation', + relationType: 'many-to-many', + entity: Proposal, + inverseSide: (it: Proposal) => it.tags + }); + return config; + } +}) +export class JobProposalPlugin implements IOnPluginBootstrap, IOnPluginDestroy, IOnPluginSeedable { + + // We disable by default additional logging for each event to avoid cluttering the logs + private logEnabled = true; + + constructor( + private readonly _proposalSeederService: ProposalSeederService + ) { } + + /** + * Called when the plugin is being initialized. + */ + onPluginBootstrap(): void | Promise { + if (this.logEnabled) { + console.log(`${JobProposalPlugin.name} is being bootstrapped...`); + } + } + + /** + * Called when the plugin is being destroyed. + */ + onPluginDestroy(): void | Promise { + if (this.logEnabled) { + console.log(`${JobProposalPlugin.name} is being destroyed...`); + } + } + + /** + * Seed default data for the plugin. + */ + async onPluginDefaultSeed() { + try { + this._proposalSeederService.createDefaultProposals(); + + if (this.logEnabled) { + console.log(chalk.green(`Default data seeded successfully for ${JobProposalPlugin.name}.`)); + } + } catch (error) { + console.error(chalk.red(`Error seeding default data for ${JobProposalPlugin.name}:`, error)); + } + } + + /** + * Seed random data for the plugin. + */ + async onPluginRandomSeed() { + try { + this._proposalSeederService.createRandomProposals(); + + if (this.logEnabled) { + console.log(chalk.green(`Random data seeded successfully for ${JobProposalPlugin.name}.`)); + } + } catch (error) { + console.error(chalk.red(`Error seeding random data for ${JobProposalPlugin.name}:`, error)); + } + } +} diff --git a/packages/core/src/employee-proposal-template/dto/create-proposal-template.dto.ts b/packages/plugins/job-proposal/src/proposal-template/dto/create-proposal-template.dto.ts similarity index 78% rename from packages/core/src/employee-proposal-template/dto/create-proposal-template.dto.ts rename to packages/plugins/job-proposal/src/proposal-template/dto/create-proposal-template.dto.ts index e6a12b94d44..d0a272c945b 100644 --- a/packages/core/src/employee-proposal-template/dto/create-proposal-template.dto.ts +++ b/packages/plugins/job-proposal/src/proposal-template/dto/create-proposal-template.dto.ts @@ -1,6 +1,6 @@ import { IEmployeeProposalTemplate } from "@gauzy/contracts"; import { IntersectionType } from "@nestjs/mapped-types"; -import { EmployeeFeatureDTO } from "./../../employee/dto"; +import { EmployeeFeatureDTO } from "@gauzy/core"; import { ProposalTemplateDTO } from "./proposal-template.dto"; /** @@ -10,4 +10,4 @@ import { ProposalTemplateDTO } from "./proposal-template.dto"; export class CreateProposalTemplateDTO extends IntersectionType( ProposalTemplateDTO, EmployeeFeatureDTO -) implements IEmployeeProposalTemplate {} \ No newline at end of file +) implements IEmployeeProposalTemplate { } diff --git a/packages/core/src/employee-proposal-template/dto/index.ts b/packages/plugins/job-proposal/src/proposal-template/dto/index.ts similarity index 100% rename from packages/core/src/employee-proposal-template/dto/index.ts rename to packages/plugins/job-proposal/src/proposal-template/dto/index.ts diff --git a/packages/core/src/employee-proposal-template/dto/proposal-template.dto.ts b/packages/plugins/job-proposal/src/proposal-template/dto/proposal-template.dto.ts similarity index 68% rename from packages/core/src/employee-proposal-template/dto/proposal-template.dto.ts rename to packages/plugins/job-proposal/src/proposal-template/dto/proposal-template.dto.ts index e174e164527..14dfdbed900 100644 --- a/packages/core/src/employee-proposal-template/dto/proposal-template.dto.ts +++ b/packages/plugins/job-proposal/src/proposal-template/dto/proposal-template.dto.ts @@ -1,7 +1,7 @@ import { IEmployeeProposalTemplate } from "@gauzy/contracts"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsBoolean, IsNotEmpty, IsOptional, IsString } from "class-validator"; -import { TenantOrganizationBaseDTO } from "./../../core/dto"; +import { TenantOrganizationBaseDTO } from "@gauzy/core"; /** * Proposal template common request DTO validation @@ -9,18 +9,18 @@ import { TenantOrganizationBaseDTO } from "./../../core/dto"; */ export class ProposalTemplateDTO extends TenantOrganizationBaseDTO implements IEmployeeProposalTemplate { - @ApiProperty({ type: () => String, readOnly: true }) + @ApiProperty({ type: () => String }) @IsNotEmpty() @IsString() readonly name: string; - @ApiPropertyOptional({ type: () => String, readOnly: true }) + @ApiPropertyOptional({ type: () => String }) @IsOptional() @IsString() readonly content: string; - @ApiPropertyOptional({ type: () => Boolean, readOnly: true }) + @ApiPropertyOptional({ type: () => Boolean }) @IsOptional() @IsBoolean() readonly isDefault: boolean; -} \ No newline at end of file +} diff --git a/packages/core/src/employee-proposal-template/dto/update-proposal-template.dto.ts b/packages/plugins/job-proposal/src/proposal-template/dto/update-proposal-template.dto.ts similarity index 84% rename from packages/core/src/employee-proposal-template/dto/update-proposal-template.dto.ts rename to packages/plugins/job-proposal/src/proposal-template/dto/update-proposal-template.dto.ts index c5e6a01b0b8..794c3ebd1ed 100644 --- a/packages/core/src/employee-proposal-template/dto/update-proposal-template.dto.ts +++ b/packages/plugins/job-proposal/src/proposal-template/dto/update-proposal-template.dto.ts @@ -5,5 +5,4 @@ import { ProposalTemplateDTO } from "./proposal-template.dto"; * Update proposal template request DTO validation * */ -export class UpdateProposalTemplateDTO extends ProposalTemplateDTO - implements IEmployeeProposalTemplate {} +export class UpdateProposalTemplateDTO extends ProposalTemplateDTO implements IEmployeeProposalTemplate { } diff --git a/packages/core/src/employee-proposal-template/employee-proposal-template.controller.spec.ts b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.controller.spec.ts similarity index 100% rename from packages/core/src/employee-proposal-template/employee-proposal-template.controller.spec.ts rename to packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.controller.spec.ts diff --git a/packages/core/src/employee-proposal-template/employee-proposal-template.controller.ts b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.controller.ts similarity index 86% rename from packages/core/src/employee-proposal-template/employee-proposal-template.controller.ts rename to packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.controller.ts index caa5585a599..a034e39b273 100644 --- a/packages/core/src/employee-proposal-template/employee-proposal-template.controller.ts +++ b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.controller.ts @@ -1,5 +1,4 @@ import { - BadRequestException, Body, Controller, Get, @@ -13,10 +12,7 @@ import { import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { UpdateResult } from 'typeorm'; import { IEmployeeProposalTemplate, IPagination, PermissionsEnum } from '@gauzy/contracts'; -import { UUIDValidationPipe, UseValidationPipe } from './../shared/pipes'; -import { PermissionGuard, TenantPermissionGuard } from './../shared/guards'; -import { Permissions } from './../shared/decorators'; -import { CrudController, PaginationParams } from './../core/crud'; +import { CrudController, Permissions, PermissionGuard, TenantPermissionGuard, UUIDValidationPipe, UseValidationPipe, PaginationParams } from '@gauzy/core'; import { EmployeeProposalTemplate } from './employee-proposal-template.entity'; import { EmployeeProposalTemplateService } from './employee-proposal-template.service'; import { CreateProposalTemplateDTO, UpdateProposalTemplateDTO } from './dto'; @@ -24,7 +20,7 @@ import { CreateProposalTemplateDTO, UpdateProposalTemplateDTO } from './dto'; @ApiTags('EmployeeProposalTemplate') @UseGuards(TenantPermissionGuard, PermissionGuard) @Permissions(PermissionsEnum.ORG_PROPOSAL_TEMPLATES_EDIT) -@Controller() +@Controller('/employee-proposal-template') export class EmployeeProposalTemplateController extends CrudController { constructor(private readonly employeeProposalTemplateService: EmployeeProposalTemplateService) { super(employeeProposalTemplateService); @@ -78,11 +74,7 @@ export class EmployeeProposalTemplateController extends CrudController ): Promise> { - try { - return await this.employeeProposalTemplateService.findAll(params); - } catch (error) { - throw new BadRequestException(error); - } + return await this.employeeProposalTemplateService.findAll(params); } /** diff --git a/packages/core/src/employee-proposal-template/employee-proposal-template.entity.ts b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.entity.ts similarity index 84% rename from packages/core/src/employee-proposal-template/employee-proposal-template.entity.ts rename to packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.entity.ts index d135655a100..70f61a45f8b 100644 --- a/packages/core/src/employee-proposal-template/employee-proposal-template.entity.ts +++ b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.entity.ts @@ -1,11 +1,7 @@ -import { RelationId } from 'typeorm'; import { ApiProperty } from '@nestjs/swagger'; +import { RelationId } from 'typeorm'; import { IEmployee, IEmployeeProposalTemplate } from '@gauzy/contracts'; -import { - Employee, - TenantOrganizationBaseEntity -} from '../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from './../core/decorators/entity'; +import { ColumnIndex, Employee, MultiORMColumn, MultiORMEntity, MultiORMManyToOne, TenantOrganizationBaseEntity } from '@gauzy/core'; import { MikroOrmEmployeeProposalTemplateRepository } from './repository/mikro-orm-employee-proposal-template.repository'; @MultiORMEntity('employee_proposal_template', { mikroOrmRepository: () => MikroOrmEmployeeProposalTemplateRepository }) @@ -32,8 +28,8 @@ export class EmployeeProposalTemplate extends TenantOrganizationBaseEntity | @ManyToOne |-------------------------------------------------------------------------- */ - @ApiProperty({ type: () => Employee }) @MultiORMManyToOne(() => Employee, { + /** Database cascade action on delete. */ onDelete: 'CASCADE' }) employee?: IEmployee; diff --git a/packages/core/src/employee-proposal-template/employee-proposal-template.module.ts b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.module.ts similarity index 59% rename from packages/core/src/employee-proposal-template/employee-proposal-template.module.ts rename to packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.module.ts index d873ecb6807..8600129d0cd 100644 --- a/packages/core/src/employee-proposal-template/employee-proposal-template.module.ts +++ b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.module.ts @@ -1,23 +1,22 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { RouterModule } from '@nestjs/core'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { RolePermissionModule } from '../role-permission/role-permission.module'; +import { RolePermissionModule } from '@gauzy/core'; import { EmployeeProposalTemplateController } from './employee-proposal-template.controller'; import { EmployeeProposalTemplate } from './employee-proposal-template.entity'; import { EmployeeProposalTemplateService } from './employee-proposal-template.service'; +import { TypeOrmEmployeeProposalTemplateRepository } from './repository'; @Module({ imports: [ - RouterModule.register([{ path: '/employee-proposal-template', module: EmployeeProposalTemplateModule }]), TypeOrmModule.forFeature([EmployeeProposalTemplate]), MikroOrmModule.forFeature([EmployeeProposalTemplate]), - forwardRef(() => RolePermissionModule), + RolePermissionModule, CqrsModule ], controllers: [EmployeeProposalTemplateController], - providers: [EmployeeProposalTemplateService], - exports: [TypeOrmModule, MikroOrmModule, EmployeeProposalTemplateService] + providers: [EmployeeProposalTemplateService, TypeOrmEmployeeProposalTemplateRepository], + exports: [] }) export class EmployeeProposalTemplateModule { } diff --git a/packages/core/src/employee-proposal-template/employee-proposal-template.service.spec.ts b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.service.spec.ts similarity index 100% rename from packages/core/src/employee-proposal-template/employee-proposal-template.service.spec.ts rename to packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.service.spec.ts diff --git a/packages/core/src/employee-proposal-template/employee-proposal-template.service.ts b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.service.ts similarity index 62% rename from packages/core/src/employee-proposal-template/employee-proposal-template.service.ts rename to packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.service.ts index e0021720c76..08bb03574e0 100644 --- a/packages/core/src/employee-proposal-template/employee-proposal-template.service.ts +++ b/packages/plugins/job-proposal/src/proposal-template/employee-proposal-template.service.ts @@ -1,26 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; import { IEmployeeProposalTemplate } from '@gauzy/contracts'; -import { PaginationParams, TenantAwareCrudService } from './../core/crud'; +import { PaginationParams, TenantAwareCrudService } from '@gauzy/core'; import { EmployeeProposalTemplate } from './employee-proposal-template.entity'; -import { TypeOrmEmployeeProposalTemplateRepository } from './repository/type-orm-employee-proposal-template.repository'; -import { MikroOrmEmployeeProposalTemplateRepository } from './repository/mikro-orm-employee-proposal-template.repository'; +import { MikroOrmEmployeeProposalTemplateRepository, TypeOrmEmployeeProposalTemplateRepository } from './repository'; @Injectable() export class EmployeeProposalTemplateService extends TenantAwareCrudService { constructor( - @InjectRepository(EmployeeProposalTemplate) - typeOrmEmployeeProposalTemplateRepository: TypeOrmEmployeeProposalTemplateRepository, - - mikroOrmEmployeeProposalTemplateRepository: MikroOrmEmployeeProposalTemplateRepository + readonly typeOrmEmployeeProposalTemplateRepository: TypeOrmEmployeeProposalTemplateRepository, + readonly mikroOrmEmployeeProposalTemplateRepository: MikroOrmEmployeeProposalTemplateRepository ) { super(typeOrmEmployeeProposalTemplateRepository, mikroOrmEmployeeProposalTemplateRepository); } /** + * Toggles the default status of a proposal template. * - * @param id - * @returns + * @param id - The ID of the proposal template. + * @returns The updated proposal template. */ async makeDefault(id: IEmployeeProposalTemplate['id']): Promise { const proposalTemplate: IEmployeeProposalTemplate = await this.findOneByIdString(id); @@ -38,8 +35,8 @@ export class EmployeeProposalTemplateService extends TenantAwareCrudService) { return await super.findAll(params); diff --git a/packages/plugins/job-proposal/src/proposal-template/repository/index.ts b/packages/plugins/job-proposal/src/proposal-template/repository/index.ts new file mode 100644 index 00000000000..bf3f1a64d50 --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal-template/repository/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-employee-proposal-template.repository'; +export * from './type-orm-employee-proposal-template.repository'; diff --git a/packages/core/src/employee-proposal-template/repository/mikro-orm-employee-proposal-template.repository.ts b/packages/plugins/job-proposal/src/proposal-template/repository/mikro-orm-employee-proposal-template.repository.ts similarity index 66% rename from packages/core/src/employee-proposal-template/repository/mikro-orm-employee-proposal-template.repository.ts rename to packages/plugins/job-proposal/src/proposal-template/repository/mikro-orm-employee-proposal-template.repository.ts index 07212d02392..ae4b10b077a 100644 --- a/packages/core/src/employee-proposal-template/repository/mikro-orm-employee-proposal-template.repository.ts +++ b/packages/plugins/job-proposal/src/proposal-template/repository/mikro-orm-employee-proposal-template.repository.ts @@ -1,4 +1,4 @@ -import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository'; +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; import { EmployeeProposalTemplate } from '../employee-proposal-template.entity'; export class MikroOrmEmployeeProposalTemplateRepository extends MikroOrmBaseEntityRepository { } diff --git a/packages/core/src/employee-proposal-template/repository/type-orm-employee-proposal-template.repository.ts b/packages/plugins/job-proposal/src/proposal-template/repository/type-orm-employee-proposal-template.repository.ts similarity index 100% rename from packages/core/src/employee-proposal-template/repository/type-orm-employee-proposal-template.repository.ts rename to packages/plugins/job-proposal/src/proposal-template/repository/type-orm-employee-proposal-template.repository.ts diff --git a/packages/plugins/job-proposal/src/proposal/commands/handlers/index.ts b/packages/plugins/job-proposal/src/proposal/commands/handlers/index.ts new file mode 100644 index 00000000000..04fa1227f10 --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/commands/handlers/index.ts @@ -0,0 +1,7 @@ +import { ProposalCreateHandler } from './proposal-create.handler'; +import { ProposalUpdateHandler } from './proposal-update.handler'; + +export const CommandHandlers = [ + ProposalCreateHandler, + ProposalUpdateHandler +]; diff --git a/packages/core/src/proposal/commands/handlers/proposal-create.handler.ts b/packages/plugins/job-proposal/src/proposal/commands/handlers/proposal-create.handler.ts similarity index 53% rename from packages/core/src/proposal/commands/handlers/proposal-create.handler.ts rename to packages/plugins/job-proposal/src/proposal/commands/handlers/proposal-create.handler.ts index 59506c6e550..7c66001d6db 100644 --- a/packages/core/src/proposal/commands/handlers/proposal-create.handler.ts +++ b/packages/plugins/job-proposal/src/proposal/commands/handlers/proposal-create.handler.ts @@ -4,10 +4,17 @@ import { ProposalService } from '../../proposal.service'; import { Proposal } from '../../proposal.entity'; @CommandHandler(ProposalCreateCommand) -export class ProposalCreateHandler - implements ICommandHandler { - constructor(private readonly _proposalService: ProposalService) {} +export class ProposalCreateHandler implements ICommandHandler { + constructor( + private readonly _proposalService: ProposalService + ) { } + /** + * Executes a command to create a proposal. + * + * @param command The command object containing the input data for creating the proposal. + * @returns A Promise that resolves to the created Proposal object. + */ public async execute(command: ProposalCreateCommand): Promise { const { input } = command; return await this._proposalService.create(input); diff --git a/packages/plugins/job-proposal/src/proposal/commands/handlers/proposal-update.handler.ts b/packages/plugins/job-proposal/src/proposal/commands/handlers/proposal-update.handler.ts new file mode 100644 index 00000000000..5482fc597a7 --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/commands/handlers/proposal-update.handler.ts @@ -0,0 +1,22 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { ProposalUpdateCommand } from '../proposal-update.command'; +import { ProposalService } from '../../proposal.service'; +import { Proposal } from '../../proposal.entity'; + +@CommandHandler(ProposalUpdateCommand) +export class ProposalUpdateHandler implements ICommandHandler { + constructor( + private readonly _proposalService: ProposalService + ) { } + + /** + * Executes the ProposalUpdateCommand to update a proposal. + * + * @param command The ProposalUpdateCommand containing the id and input data for the update. + * @returns A Promise that resolves to the updated Proposal entity. + */ + public async execute(command: ProposalUpdateCommand): Promise { + const { id, input } = command; + return await this._proposalService.create({ ...input, id }); + } +} diff --git a/packages/plugins/job-proposal/src/proposal/commands/index.ts b/packages/plugins/job-proposal/src/proposal/commands/index.ts new file mode 100644 index 00000000000..45b0f6205f4 --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/commands/index.ts @@ -0,0 +1,2 @@ +export * from './proposal-create.command'; +export * from './proposal-update.command'; diff --git a/packages/core/src/proposal/commands/proposal-create.command.ts b/packages/plugins/job-proposal/src/proposal/commands/proposal-create.command.ts similarity index 77% rename from packages/core/src/proposal/commands/proposal-create.command.ts rename to packages/plugins/job-proposal/src/proposal/commands/proposal-create.command.ts index ef10c9699e2..9ee32ae442c 100644 --- a/packages/core/src/proposal/commands/proposal-create.command.ts +++ b/packages/plugins/job-proposal/src/proposal/commands/proposal-create.command.ts @@ -4,5 +4,5 @@ import { IProposalCreateInput } from '@gauzy/contracts'; export class ProposalCreateCommand implements ICommand { static readonly type = '[Proposal] Create Proposal'; - constructor(public readonly input: IProposalCreateInput) {} + constructor(public readonly input: IProposalCreateInput) { } } diff --git a/packages/plugins/job-proposal/src/proposal/commands/proposal-update.command.ts b/packages/plugins/job-proposal/src/proposal/commands/proposal-update.command.ts new file mode 100644 index 00000000000..33ab4e0511c --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/commands/proposal-update.command.ts @@ -0,0 +1,11 @@ +import { ICommand } from '@nestjs/cqrs'; +import { IProposal, IProposalCreateInput as IProposalUpdateInput } from '@gauzy/contracts'; + +export class ProposalUpdateCommand implements ICommand { + static readonly type = '[Proposal] Update Proposal'; + + constructor( + public readonly id: IProposal['id'], + public readonly input: IProposalUpdateInput + ) { } +} diff --git a/packages/core/src/proposal/dto/create-proposal.dto.ts b/packages/plugins/job-proposal/src/proposal/dto/create-proposal.dto.ts similarity index 70% rename from packages/core/src/proposal/dto/create-proposal.dto.ts rename to packages/plugins/job-proposal/src/proposal/dto/create-proposal.dto.ts index 0488be83034..d31652e67b9 100644 --- a/packages/core/src/proposal/dto/create-proposal.dto.ts +++ b/packages/plugins/job-proposal/src/proposal/dto/create-proposal.dto.ts @@ -1,7 +1,6 @@ -import { IProposalCreateInput } from "@gauzy/contracts"; import { IntersectionType, PartialType } from "@nestjs/mapped-types"; -import { EmployeeFeatureDTO } from "./../../employee/dto"; -import { RelationalTagDTO } from "./../../tags/dto"; +import { IProposalCreateInput } from "@gauzy/contracts"; +import { EmployeeFeatureDTO, RelationalTagDTO } from "@gauzy/core"; import { ProposalDTO } from "./proposal.dto"; /** @@ -11,4 +10,4 @@ export class CreateProposalDTO extends IntersectionType( ProposalDTO, PartialType(EmployeeFeatureDTO), RelationalTagDTO -) implements IProposalCreateInput {} \ No newline at end of file +) implements IProposalCreateInput { } diff --git a/packages/core/src/proposal/dto/index.ts b/packages/plugins/job-proposal/src/proposal/dto/index.ts similarity index 100% rename from packages/core/src/proposal/dto/index.ts rename to packages/plugins/job-proposal/src/proposal/dto/index.ts diff --git a/packages/core/src/proposal/dto/proposal.dto.ts b/packages/plugins/job-proposal/src/proposal/dto/proposal.dto.ts similarity index 54% rename from packages/core/src/proposal/dto/proposal.dto.ts rename to packages/plugins/job-proposal/src/proposal/dto/proposal.dto.ts index 7019751f81a..320e9b4a829 100644 --- a/packages/core/src/proposal/dto/proposal.dto.ts +++ b/packages/plugins/job-proposal/src/proposal/dto/proposal.dto.ts @@ -1,40 +1,40 @@ -import { IOrganizationContact, ProposalStatusEnum } from "@gauzy/contracts"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsEnum, IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator"; -import { TenantOrganizationBaseDTO } from "./../../core/dto"; +import { IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID } from "class-validator"; +import { IOrganizationContact, ProposalStatusEnum } from "@gauzy/contracts"; +import { TenantOrganizationBaseDTO } from "@gauzy/core"; export class ProposalDTO extends TenantOrganizationBaseDTO { - @ApiPropertyOptional({ type: () => String, readOnly: true }) + @ApiPropertyOptional({ type: () => String }) @IsOptional() @IsString() readonly jobPostUrl: string; - @ApiProperty({ type: () => Date, readOnly: true }) + @ApiProperty({ type: () => Date }) @IsNotEmpty() readonly valueDate: Date; - @ApiProperty({ type: () => String, readOnly: true }) + @ApiProperty({ type: () => String }) @IsNotEmpty() @IsString() readonly jobPostContent: string; - @ApiProperty({ type: () => String, readOnly: true }) + @ApiProperty({ type: () => String }) @IsNotEmpty() @IsString() readonly proposalContent: string; - @ApiProperty({ type: () => String, enum: ProposalStatusEnum, readOnly: true }) + @ApiProperty({ type: () => String, enum: ProposalStatusEnum }) @IsEnum(ProposalStatusEnum) readonly status: ProposalStatusEnum = ProposalStatusEnum.SENT; - @ApiPropertyOptional({ type: () => Object, readOnly: true }) + @ApiPropertyOptional({ type: () => Object }) @IsOptional() @IsObject() readonly organizationContact: IOrganizationContact; - @ApiPropertyOptional({ type: () => String, readOnly: true }) + @ApiPropertyOptional({ type: () => String }) @IsOptional() - @IsString() + @IsUUID() readonly organizationContactId: string; -} \ No newline at end of file +} diff --git a/packages/core/src/proposal/dto/update-proposal.dto.ts b/packages/plugins/job-proposal/src/proposal/dto/update-proposal.dto.ts similarity index 80% rename from packages/core/src/proposal/dto/update-proposal.dto.ts rename to packages/plugins/job-proposal/src/proposal/dto/update-proposal.dto.ts index 642e4eed2b2..d2f882af006 100644 --- a/packages/core/src/proposal/dto/update-proposal.dto.ts +++ b/packages/plugins/job-proposal/src/proposal/dto/update-proposal.dto.ts @@ -1,6 +1,6 @@ import { IProposalCreateInput } from "@gauzy/contracts"; import { IntersectionType, OmitType, PartialType } from "@nestjs/mapped-types"; -import { RelationalTagDTO } from "./../../tags/dto"; +import { RelationalTagDTO } from "@gauzy/core"; import { ProposalDTO } from "./proposal.dto"; /** @@ -9,4 +9,4 @@ import { ProposalDTO } from "./proposal.dto"; export class UpdateProposalDTO extends IntersectionType( PartialType(OmitType(ProposalDTO, ['valueDate'] as const)), RelationalTagDTO -) implements IProposalCreateInput {} \ No newline at end of file +) implements IProposalCreateInput { } diff --git a/packages/plugins/job-proposal/src/proposal/proposal-seeder.service.ts b/packages/plugins/job-proposal/src/proposal/proposal-seeder.service.ts new file mode 100644 index 00000000000..70d801aabd5 --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/proposal-seeder.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { ConnectionEntityManager, SeedDataService, randomSeedConfig } from '@gauzy/core'; +import { createDefaultProposals, createRandomProposals } from './proposal.seed'; + +/** + * Service dealing with help center based operations. + * + * @class + */ +@Injectable() +export class ProposalSeederService { + /** + * Create an instance of class. + * + * @constructs + * + */ + constructor( + private readonly _connectionEntityManager: ConnectionEntityManager, + private readonly _seeder: SeedDataService + ) { } + + /** + * Creates default proposals for organizations. + * + * @returns A Promise that resolves when the default proposals are created. + */ + async createDefaultProposals(): Promise { + await createDefaultProposals( + this._connectionEntityManager.rawConnection, + this._seeder.tenant, + this._seeder.defaultEmployees, + this._seeder.organizations, + randomSeedConfig.proposalsSharingPerOrganizations || 30 + ); + } + + /** + * Creates random proposals for organizations. + * + * @returns A Promise that resolves when the random proposals are created. + */ + async createRandomProposals(): Promise { + await createRandomProposals( + this._connectionEntityManager.rawConnection, + this._seeder.randomTenants, + this._seeder.randomTenantOrganizationsMap, + this._seeder.randomOrganizationEmployeesMap, + randomSeedConfig.proposalsSharingPerOrganizations || 30 + ); + } +} diff --git a/packages/plugins/job-proposal/src/proposal/proposal.controller.ts b/packages/plugins/job-proposal/src/proposal/proposal.controller.ts new file mode 100644 index 00000000000..8385564a578 --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/proposal.controller.ts @@ -0,0 +1,167 @@ +import { + Controller, + HttpStatus, + Post, + Body, + Get, + Query, + UseGuards, + Put, + Param +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { CommandBus } from '@nestjs/cqrs'; +import { IProposal, IPagination, PermissionsEnum } from '@gauzy/contracts'; +import { + CrudController, + Permissions, + PermissionGuard, + TenantPermissionGuard, + UseValidationPipe, + PaginationParams, + ParseJsonPipe, + UUIDValidationPipe, + OptionParams +} from '@gauzy/core'; +import { ProposalCreateCommand, ProposalUpdateCommand } from './commands'; +import { ProposalService } from './proposal.service'; +import { Proposal } from './proposal.entity'; +import { CreateProposalDTO, UpdateProposalDTO } from './dto'; + +@ApiTags('Proposal') +@UseGuards(TenantPermissionGuard, PermissionGuard) +@Permissions(PermissionsEnum.ORG_PROPOSALS_EDIT) +@Controller('/proposal') +export class ProposalController extends CrudController { + + constructor( + private readonly _proposalService: ProposalService, + private readonly _commandBus: CommandBus + ) { + super(_proposalService); + } + + /** + * Get proposals using pagination. + * + * @param params The pagination parameters. + * @returns The paginated list of proposals. + */ + @Permissions(PermissionsEnum.ORG_PROPOSALS_VIEW) + @ApiOperation({ summary: 'Get proposals by pagination' }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Proposals retrieved successfully', + type: Proposal + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid pagination parameters provided', + }) + @Get('pagination') + @UseValidationPipe({ transform: true }) + async pagination(@Query() params: PaginationParams): Promise> { + return await this._proposalService.pagination(params); + } + + /** + * Find all proposals based on the provided options. + * + * @param data The options for finding proposals. + * @returns The found proposals. + */ + @ApiOperation({ summary: 'Find all proposals' }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Proposals found', + type: Proposal + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'No proposals found', + }) + @Permissions(PermissionsEnum.ORG_PROPOSALS_VIEW) + @Get() + async findAll(@Query('data', ParseJsonPipe) data: any): Promise> { + const { relations, findInput, filterDate } = data; + return await this._proposalService.getAllProposals({ where: findInput, relations }, filterDate); + } + + /** + * Find a single proposal by its ID. + * + * @param id The ID of the proposal to find. + * @param options Additional options for the query. + * @returns The found proposal. + */ + @ApiOperation({ summary: 'Find a single proposal by ID' }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Proposal found', + type: Proposal + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Proposal not found', + }) + @Permissions(PermissionsEnum.ORG_PROPOSALS_VIEW) + @Get(':id') + async findById( + @Param('id', UUIDValidationPipe) id: string, + @Query() options: OptionParams, + ): Promise { + return await this._proposalService.findOneByIdString(id, { relations: options.relations || [] }); + } + + /** + * Create a new proposal record. + * + * @param entity The data to create the proposal. + * @returns The newly created proposal. + */ + @ApiOperation({ summary: 'Create a new proposal record' }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Proposal created successfully', + type: Proposal + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input. The response body may contain clues as to what went wrong', + }) + @Post() + @UseValidationPipe({ transform: true, whitelist: true }) + async create(@Body() entity: CreateProposalDTO): Promise { + return await this._commandBus.execute( + new ProposalCreateCommand(entity), + ); + } + + /** + * Update a single proposal by its ID. + * + * @param id The ID of the proposal to update. + * @param entity The updated proposal data. + * @returns The updated proposal. + */ + @ApiOperation({ summary: 'Update a single proposal by ID' }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Proposal updated successfully', + type: Proposal + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Proposal not found', + }) + @Put(':id') + @UseValidationPipe({ transform: true, whitelist: true }) + async update( + @Param('id', UUIDValidationPipe) id: string, + @Body() entity: UpdateProposalDTO, + ): Promise { + return await this._commandBus.execute( + new ProposalUpdateCommand(id, entity), + ); + } +} diff --git a/packages/core/src/proposal/proposal.entity.ts b/packages/plugins/job-proposal/src/proposal/proposal.entity.ts similarity index 60% rename from packages/core/src/proposal/proposal.entity.ts rename to packages/plugins/job-proposal/src/proposal/proposal.entity.ts index 2c4b9e5d925..febb4146966 100644 --- a/packages/core/src/proposal/proposal.entity.ts +++ b/packages/plugins/job-proposal/src/proposal/proposal.entity.ts @@ -4,26 +4,29 @@ import { JoinTable } from 'typeorm'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsUUID } from 'class-validator'; import { IProposal, IEmployee, - ITag, IOrganizationContact, ProposalStatusEnum } from '@gauzy/contracts'; import { + ColumnIndex, Employee, + MultiORMColumn, + MultiORMEntity, + MultiORMManyToMany, + MultiORMManyToOne, OrganizationContact, Tag, + Taggable, TenantOrganizationBaseEntity -} from '../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToMany, MultiORMManyToOne } from './../core/decorators/entity'; +} from '@gauzy/core'; import { MikroOrmProposalRepository } from './repository/mikro-orm-proposal.repository'; @MultiORMEntity('proposal', { mikroOrmRepository: () => MikroOrmProposalRepository }) -export class Proposal extends TenantOrganizationBaseEntity - implements IProposal { - +export class Proposal extends TenantOrganizationBaseEntity implements IProposal, Taggable { @ApiProperty({ type: () => String }) @ColumnIndex() @MultiORMColumn({ nullable: true }) @@ -50,26 +53,41 @@ export class Proposal extends TenantOrganizationBaseEntity | @ManyToOne |-------------------------------------------------------------------------- */ + /** + * + */ + @MultiORMManyToOne(() => Employee, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true, - @ApiProperty({ type: () => Employee }) - @MultiORMManyToOne(() => Employee, { nullable: true, onDelete: 'CASCADE' }) + /** Database cascade action on delete. */ + onDelete: 'CASCADE' + }) @JoinColumn() employee: IEmployee; - @ApiProperty({ type: () => String, readOnly: true }) + @ApiProperty({ type: () => String }) + @IsUUID() @RelationId((it: Proposal) => it.employee) @MultiORMColumn({ nullable: true, relationId: true }) employeeId?: string; - @ApiPropertyOptional({ type: () => OrganizationContact }) - @MultiORMManyToOne(() => OrganizationContact, (organizationContact) => organizationContact.proposals, { + /** + * + */ + @MultiORMManyToOne(() => OrganizationContact, { + /** Indicates if relation column value can be nullable or not. */ nullable: true, + + /** Database cascade action on delete. */ onDelete: 'CASCADE' }) @JoinColumn() organizationContact?: IOrganizationContact; - @ApiProperty({ type: () => String }) + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() @RelationId((it: Proposal) => it.organizationContact) @MultiORMColumn({ nullable: true, relationId: true }) organizationContactId?: string; @@ -79,18 +97,25 @@ export class Proposal extends TenantOrganizationBaseEntity | @ManyToMany |-------------------------------------------------------------------------- */ - // Tags - @ApiProperty({ type: () => Tag }) - @MultiORMManyToMany(() => Tag, (tag) => tag.proposals, { + /** + * Tags + */ + @MultiORMManyToMany(() => Tag, (it: Tag) => it.customFields['proposals'], { + /** Database cascade action on update. */ onUpdate: 'CASCADE', + /** Database cascade action on delete. */ onDelete: 'CASCADE', + /** This column is a boolean flag indicating whether the current entity is the 'owning' side of a relationship. */ owner: true, + /** Pivot table for many-to-many relationship. */ pivotTable: 'tag_proposal', + /** Column in pivot table referencing 'proposal' primary key. */ joinColumn: 'proposalId', - inverseJoinColumn: 'tagId', + /** Column in pivot table referencing 'tag' primary key. */ + inverseJoinColumn: 'tagId' }) @JoinTable({ name: 'tag_proposal' }) - tags?: ITag[]; + tags?: Tag[]; } diff --git a/packages/plugins/job-proposal/src/proposal/proposal.moule.ts b/packages/plugins/job-proposal/src/proposal/proposal.moule.ts new file mode 100644 index 00000000000..253a31334cd --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/proposal.moule.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CqrsModule } from '@nestjs/cqrs'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { RolePermissionModule, SeederModule } from '@gauzy/core'; +import { Proposal } from './proposal.entity'; +import { ProposalController } from './proposal.controller'; +import { ProposalService } from './proposal.service'; +import { CommandHandlers } from './commands/handlers'; +import { ProposalSeederService } from './proposal-seeder.service'; +import { TypeOrmProposalRepository } from './repository'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Proposal]), + MikroOrmModule.forFeature([Proposal]), + RolePermissionModule, + SeederModule, + CqrsModule + ], + controllers: [ProposalController], + providers: [ProposalService, ProposalSeederService, TypeOrmProposalRepository, ...CommandHandlers], + exports: [ProposalSeederService] +}) +export class ProposalModule { } diff --git a/packages/plugins/job-proposal/src/proposal/proposal.seed.ts b/packages/plugins/job-proposal/src/proposal/proposal.seed.ts new file mode 100644 index 00000000000..123e19ad28e --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/proposal.seed.ts @@ -0,0 +1,140 @@ +import { DataSource } from 'typeorm'; +import { Proposal } from './proposal.entity'; +import { faker } from '@faker-js/faker'; +import * as moment from 'moment'; +import { OrganizationContact, Tag } from '@gauzy/core'; +import { IEmployee, IOrganization, ITenant, ProposalStatusEnum } from '@gauzy/contracts'; + +/** + * Creates default proposals for organizations. + * + * @param connection The database connection. + * @param tenant The tenant information. + * @param employees The list of employees. + * @param organizations The list of organizations. + * @param noOfProposalsPerOrganization The number of proposals to create per organization. + * @returns A promise that resolves to an array of created proposals. + */ +export const createDefaultProposals = async ( + connection: DataSource, + tenant: ITenant, + employees: IEmployee[], + organizations: IOrganization[], + noOfProposalsPerOrganization: number +): Promise => { + + const proposals: Proposal[] = []; + const tagsMap = new Map(); + const organizationContactsMap = new Map(); + + // Fetch tags and organization contacts for each organization asynchronously + await Promise.all( + organizations.map(async (organization) => { + const tags = await connection.manager.findBy(Tag, { organizationId: organization.id }); + const organizationContacts = await connection.manager.findBy(OrganizationContact, { + organizationId: organization.id, + tenantId: tenant.id + }); + tagsMap.set(organization.id, tags); + organizationContactsMap.set(organization.id, organizationContacts); + }) + ); + + // Generate proposals for each organization + organizations.forEach((organization) => { + const tags = tagsMap.get(organization.id) || []; + const organizationContacts = organizationContactsMap.get(organization.id) || []; + for (let i = 0; i < noOfProposalsPerOrganization; i++) { + const proposal = new Proposal(); + proposal.employee = faker.helpers.arrayElement(employees); + proposal.jobPostUrl = faker.internet.url(); + proposal.jobPostContent = faker.person.jobTitle(); + proposal.organization = organization; + proposal.status = faker.helpers.arrayElement(Object.values(ProposalStatusEnum)); + proposal.tags = [faker.helpers.arrayElement(tags)]; + proposal.valueDate = moment(faker.date.recent({ days: 0.5 })).startOf('day').toDate(); + proposal.proposalContent = faker.person.jobDescriptor(); + proposal.tenant = tenant; + if (organizationContacts.length) { + proposal.organizationContactId = faker.helpers.arrayElement(organizationContacts).id; + } + proposals.push(proposal); + } + }); + + // Save generated proposals + return await connection.manager.save(proposals, { chunk: 30 }); +}; + +/** + * Creates random proposals for organizations across multiple tenants. + * + * @param connection The database connection. + * @param tenants An array of tenants. + * @param tenantOrganizationsMap A map containing organizations for each tenant. + * @param organizationEmployeesMap A map containing employees for each organization. + * @param noOfProposalsPerOrganization The number of proposals to create per organization. + * @returns A Promise that resolves with the created proposals. + */ +export const createRandomProposals = async ( + connection: DataSource, + tenants: ITenant[], + tenantOrganizationsMap: Map, + organizationEmployeesMap: Map, + noOfProposalsPerOrganization: number +): Promise => { + const proposals: Proposal[] = []; + + // Pre-fetch tags and organization contacts for all organizations + const organizationTagsMap: Map = new Map(); + const organizationContactsMap: Map = new Map(); + + for (const [tenant, organizations] of tenantOrganizationsMap.entries()) { + for (const organization of organizations) { + const { id: tenantId } = tenant; + const { id: organizationId } = organization; + + const tags = await connection.manager.findBy(Tag, { organizationId, tenantId }); + organizationTagsMap.set(organizationId, tags); + + const contacts = await connection.manager.findBy(OrganizationContact, { organizationId, tenantId }); + organizationContactsMap.set(organizationId, contacts); + } + } + + // Generate proposals for each organization + for (const tenant of tenants) { + const organizations = tenantOrganizationsMap.get(tenant); + if (!organizations) continue; + + for (const organization of organizations) { + const employees = organizationEmployeesMap.get(organization); + if (!employees) continue; + + const { id: organizationId } = organization; + + const tags = organizationTagsMap.get(organizationId) || []; + const organizationContacts = organizationContactsMap.get(organizationId) || []; + + for (let i = 0; i < noOfProposalsPerOrganization; i++) { + const proposal = new Proposal(); + proposal.employee = faker.helpers.arrayElement(employees); + proposal.jobPostUrl = faker.internet.url(); + proposal.jobPostContent = faker.person.jobTitle(); + proposal.status = faker.helpers.arrayElement(Object.values(ProposalStatusEnum)); + proposal.tags = [faker.helpers.arrayElement(tags)]; + proposal.valueDate = moment(faker.date.recent({ days: 0.5 })).startOf('day').toDate(); + proposal.proposalContent = faker.person.jobDescriptor(); + proposal.organization = organization; + proposal.tenant = tenant; + if (organizationContacts.length) { + proposal.organizationContact = faker.helpers.arrayElement(organizationContacts); + } + proposals.push(proposal); + } + } + } + + // Save the generated proposals in batches + return await connection.manager.save(proposals, { chunk: 30 }); +}; diff --git a/packages/plugins/job-proposal/src/proposal/proposal.service.ts b/packages/plugins/job-proposal/src/proposal/proposal.service.ts new file mode 100644 index 00000000000..b7f6a2e3637 --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/proposal.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@nestjs/common'; +import { FindManyOptions, Between, Raw } from 'typeorm'; +import * as moment from 'moment'; +import { IProposal, IPagination } from '@gauzy/contracts'; +import { isPostgres } from '@gauzy/config'; +import { TenantAwareCrudService } from '@gauzy/core'; +import { Proposal } from './proposal.entity'; +import { MikroOrmProposalRepository, TypeOrmProposalRepository } from './repository'; + +@Injectable() +export class ProposalService extends TenantAwareCrudService { + constructor( + readonly typeOrmProposalRepository: TypeOrmProposalRepository, + readonly mikroOrmProposalRepository: MikroOrmProposalRepository + ) { + super(typeOrmProposalRepository, mikroOrmProposalRepository); + } + + /** + * + * @param filter + * @param filterDate + * @returns + */ + async getAllProposals( + filter?: FindManyOptions, filterDate?: string + ): Promise> { + const total = await this.typeOrmRepository.count(filter); + let items = await this.typeOrmRepository.find(filter); + + if (filterDate) { + const dateObject = new Date(filterDate); + + const month = dateObject.getMonth() + 1; + const year = dateObject.getFullYear(); + + items = items.filter((i) => { + const currentItemMonth = i.valueDate.getMonth() + 1; + const currentItemYear = i.valueDate.getFullYear(); + return currentItemMonth === month && currentItemYear === year; + }); + } + + return { items, total }; + } + + /** + * Paginates data based on the provided filter options. + * + * @param filter The filter options for pagination. + * @returns The paginated data. + */ + public async pagination(filter: FindManyOptions) { + // Check if 'where' property exists in the filter + if (!('where' in filter)) { + // If 'where' property is missing, return paginated data without any modification + return await super.paginate(filter); + } + + const { where } = filter; + + // Determine the appropriate like operator based on the database type + const likeOperator = isPostgres() ? 'ILIKE' : 'LIKE'; + + if ('valueDate' in where) { + const { valueDate } = where; + // If 'valueDate' property exists, extract start and end dates + const { startDate, endDate } = valueDate; + + // Get the start and end of the current month in UTC format + const startOfCurrentMonth = moment().startOf('month').utc().format('YYYY-MM-DD HH:mm:ss'); + const endOfCurrentMonth = moment().endOf('month').utc().format('YYYY-MM-DD HH:mm:ss'); + + // Update the 'valueDate' property to filter records between the specified dates + filter['where']['valueDate'] = Between( + startDate ? moment.utc(startDate).format('YYYY-MM-DD HH:mm:ss') : startOfCurrentMonth, + endDate ? moment.utc(endDate).format('YYYY-MM-DD HH:mm:ss') : endOfCurrentMonth + ); + } + + // Check if 'jobPostContent' property exists in the 'where' filter + if ('jobPostContent' in where) { + // If 'jobPostContent' property exists, construct a raw SQL query to perform a like search + const { jobPostContent } = where; + filter['where']['jobPostContent'] = Raw(alias => `${alias} ${likeOperator} '%${jobPostContent}%'`); + } + + // Return the paginated data after applying any modifications + return await super.paginate(filter); + } +} diff --git a/packages/plugins/job-proposal/src/proposal/repository/index.ts b/packages/plugins/job-proposal/src/proposal/repository/index.ts new file mode 100644 index 00000000000..080ca6c8733 --- /dev/null +++ b/packages/plugins/job-proposal/src/proposal/repository/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-proposal.repository'; +export * from './type-orm-proposal.repository'; diff --git a/packages/core/src/proposal/repository/mikro-orm-proposal.repository.ts b/packages/plugins/job-proposal/src/proposal/repository/mikro-orm-proposal.repository.ts similarity index 57% rename from packages/core/src/proposal/repository/mikro-orm-proposal.repository.ts rename to packages/plugins/job-proposal/src/proposal/repository/mikro-orm-proposal.repository.ts index f85a540b5ed..9337c6d2a53 100644 --- a/packages/core/src/proposal/repository/mikro-orm-proposal.repository.ts +++ b/packages/plugins/job-proposal/src/proposal/repository/mikro-orm-proposal.repository.ts @@ -1,4 +1,4 @@ -import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository'; +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; import { Proposal } from '../proposal.entity'; export class MikroOrmProposalRepository extends MikroOrmBaseEntityRepository { } diff --git a/packages/core/src/proposal/repository/type-orm-proposal.repository.ts b/packages/plugins/job-proposal/src/proposal/repository/type-orm-proposal.repository.ts similarity index 100% rename from packages/core/src/proposal/repository/type-orm-proposal.repository.ts rename to packages/plugins/job-proposal/src/proposal/repository/type-orm-proposal.repository.ts diff --git a/packages/plugins/job-proposal/tsconfig.build.json b/packages/plugins/job-proposal/tsconfig.build.json new file mode 100644 index 00000000000..ea6be8e9a50 --- /dev/null +++ b/packages/plugins/job-proposal/tsconfig.build.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.json" +} diff --git a/packages/plugins/job-proposal/tsconfig.json b/packages/plugins/job-proposal/tsconfig.json new file mode 100644 index 00000000000..999d1cb59b2 --- /dev/null +++ b/packages/plugins/job-proposal/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "sourceMap": true, + "baseUrl": "./src", + "rootDir": "./src", + "outDir": "./dist", + "types": ["node", "jest"] + }, + "include": ["./src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/plugins/job-proposal/tsconfig.spec.json b/packages/plugins/job-proposal/tsconfig.spec.json new file mode 100644 index 00000000000..9f405553401 --- /dev/null +++ b/packages/plugins/job-proposal/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/packages/plugins/job-proposal/tslint.json b/packages/plugins/job-proposal/tslint.json new file mode 100644 index 00000000000..74516cfb290 --- /dev/null +++ b/packages/plugins/job-proposal/tslint.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tslint.json", + "rules": [] +} diff --git a/packages/plugins/job-search/.dockerignore b/packages/plugins/job-search/.dockerignore new file mode 100644 index 00000000000..b88b16df035 --- /dev/null +++ b/packages/plugins/job-search/.dockerignore @@ -0,0 +1,25 @@ +# Git +.git +.gitmodules +.gitignore + +# README file containing information about the project +README.md + +# Directory containing Docker-related files and configurations +docker + +# Directory containing installed Node.js modules, managed by npm or yarn +node_modules + +# Directory for temporary files +tmp + +# Directory for build-related files +build + +# Directory for distribution files, typically production-ready code +dist + +# Environment variables configuration file (dotenv file) +.env diff --git a/packages/plugins/job-search/README.md b/packages/plugins/job-search/README.md new file mode 100644 index 00000000000..9824721f417 --- /dev/null +++ b/packages/plugins/job-search/README.md @@ -0,0 +1,20 @@ +# Job Search Plugin + +## Overview + +The Job Search Plugin is a powerful tool designed to enhance your job search experience. Whether you're a job seeker or an employer, this plugin offers features to streamline the process and improve efficiency. + +## Features + +- **Advanced Search:** Utilize advanced search filters to find relevant job listings tailored to your preferences. +- **Job Alerts:** Set up job alerts to receive notifications when new job listings matching your criteria are posted. + +## Installation + +To install the Job Search Plugin, simply run the following command in your terminal: + +```bash +npm install @gauzy/job-search-plugin +# or +yarn add @gauzy/job-search-plugin +``` diff --git a/packages/plugins/job-search/jest.config.js b/packages/plugins/job-search/jest.config.js new file mode 100644 index 00000000000..c1859382308 --- /dev/null +++ b/packages/plugins/job-search/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + name: 'job-search', + preset: '../../../jest.config.js', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + coverageDirectory: '../../coverage/plugins/job-search' +}; diff --git a/packages/plugins/job-search/package.json b/packages/plugins/job-search/package.json new file mode 100644 index 00000000000..d03f513e3bf --- /dev/null +++ b/packages/plugins/job-search/package.json @@ -0,0 +1,48 @@ +{ + "name": "@gauzy/job-search-plugin", + "version": "0.1.0", + "description": "Ever Gauzy Platform Job Search Plugin", + "author": { + "name": "Ever Co. LTD", + "email": "ever@ever.co", + "url": "https://ever.co" + }, + "license": "AGPL-3.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "directories": { + "lib": "dist", + "test": "__test__" + }, + "publishConfig": { + "access": "restricted" + }, + "scripts": { + "test:e2e": "jest --config ./jest.config.js", + "build": "rimraf dist && yarn run compile", + "compile": "tsc -p tsconfig.build.json" + }, + "keywords": [], + "dependencies": { + "@faker-js/faker": "8.0.0-alpha.0", + "@gauzy/config": "^0.1.0", + "@gauzy/contracts": "^0.1.0", + "@gauzy/core": "^0.1.0", + "@gauzy/integration-ai": "^0.1.0", + "@gauzy/plugin": "^0.1.0", + "@nestjs/common": "^10.3.7", + "@nestjs/swagger": "^7.3.0", + "chalk": "4.1.2", + "html-to-text": "^9.0.5", + "typeorm": "^0.3.20" + }, + "devDependencies": { + "@types/node": "^17.0.33", + "rimraf": "^3.0.2", + "typescript": "5.1.6" + } +} diff --git a/packages/core/src/employee-job-preset/commands/create-job-preset.command.ts b/packages/plugins/job-search/src/employee-job-preset/commands/create-job-preset.command.ts similarity index 77% rename from packages/core/src/employee-job-preset/commands/create-job-preset.command.ts rename to packages/plugins/job-search/src/employee-job-preset/commands/create-job-preset.command.ts index d02f704f24e..d5d3ad5e196 100644 --- a/packages/core/src/employee-job-preset/commands/create-job-preset.command.ts +++ b/packages/plugins/job-search/src/employee-job-preset/commands/create-job-preset.command.ts @@ -4,5 +4,7 @@ import { ICommand } from '@nestjs/cqrs'; export class CreateJobPresetCommand implements ICommand { static readonly type = '[JobPreset] Create'; - constructor(public readonly input?: IJobPreset) {} + constructor( + public readonly input: IJobPreset + ) { } } diff --git a/packages/core/src/employee-job-preset/commands/handlers/create-job-preset.handler.ts b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/create-job-preset.handler.ts similarity index 60% rename from packages/core/src/employee-job-preset/commands/handlers/create-job-preset.handler.ts rename to packages/plugins/job-search/src/employee-job-preset/commands/handlers/create-job-preset.handler.ts index 1172eb90517..d64b7e29aad 100644 --- a/packages/core/src/employee-job-preset/commands/handlers/create-job-preset.handler.ts +++ b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/create-job-preset.handler.ts @@ -1,69 +1,68 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { InjectRepository } from '@nestjs/typeorm'; -import { RequestContext } from '../../../core/context'; -import { Employee } from '../../../employee/employee.entity'; +import { RequestContext, TypeOrmEmployeeRepository } from '@gauzy/core'; import { JobPresetUpworkJobSearchCriterion } from '../../job-preset-upwork-job-search-criterion.entity'; import { JobPreset } from '../../job-preset.entity'; import { CreateJobPresetCommand } from '../create-job-preset.command'; import { TypeOrmJobPresetRepository } from '../../repository/type-orm-job-preset.repository'; import { TypeOrmJobPresetUpworkJobSearchCriterionRepository } from '../../repository/type-orm-job-preset-upwork-job-search-criterion.repository'; -import { TypeOrmEmployeeRepository } from '../../../employee/repository/type-orm-employee.repository'; @CommandHandler(CreateJobPresetCommand) export class CreateJobPresetHandler implements ICommandHandler { constructor( - @InjectRepository(JobPreset) private readonly typeOrmJobPresetRepository: TypeOrmJobPresetRepository, - - @InjectRepository(Employee) private readonly typeOrmEmployeeRepository: TypeOrmEmployeeRepository, - - @InjectRepository(JobPresetUpworkJobSearchCriterion) private readonly typeOrmJobPresetUpworkJobSearchCriterionRepository: TypeOrmJobPresetUpworkJobSearchCriterionRepository ) { } + /** + * Executes the command to create a job preset. + * + * @param command The command containing the input data for creating the job preset. + * @returns A Promise that resolves to the created job preset. + */ public async execute(command: CreateJobPresetCommand): Promise { const { input } = command; + // Set tenantId + input.tenantId = RequestContext.currentTenantId(); + + // Set organizationId if not provided in the input if (!input.organizationId) { - const user = RequestContext.currentUser(); - const employee = await this.typeOrmEmployeeRepository.findOneBy({ - id: user.employeeId - }); - input.organizationId = employee.organizationId; + const employeeId = RequestContext.currentEmployeeId(); + if (employeeId) { + const employee = await this.typeOrmEmployeeRepository.findOneBy({ id: employeeId }); + input.organizationId = employee.organizationId; + } } - input.tenantId = RequestContext.currentTenantId(); + // Create a new job preset const jobPreset = new JobPreset(input); - - delete jobPreset.jobPresetCriterions; + delete jobPreset.jobPresetCriterions; // Remove jobPresetCriterions property from input await this.typeOrmJobPresetRepository.save(jobPreset); + // Prepare job preset criteria let jobPresetCriterion: JobPresetUpworkJobSearchCriterion[] = []; - if (input.jobPresetCriterions && input.jobPresetCriterions.length > 0) { - jobPresetCriterion = input.jobPresetCriterions.map( - (criterion) => - new JobPresetUpworkJobSearchCriterion({ - ...criterion, - jobPresetId: jobPreset.id - }) + jobPresetCriterion = input.jobPresetCriterions.map((criterion) => + new JobPresetUpworkJobSearchCriterion({ + ...criterion, + jobPresetId: jobPreset.id + }) ); + // Delete existing job preset criteria await this.typeOrmJobPresetUpworkJobSearchCriterionRepository.delete({ jobPresetId: jobPreset.id }); - await this.typeOrmJobPresetUpworkJobSearchCriterionRepository.save( - jobPresetCriterion - ); + // Save new job preset criteria + await this.typeOrmJobPresetUpworkJobSearchCriterionRepository.save(jobPresetCriterion); + // Update job preset with the new job preset criteria jobPreset.jobPresetCriterions = jobPresetCriterion; - - // jobPreset.jobPresetCriterion = jobPresetCriterion; - // await this.jobPresetRepository.save(jobPreset); } + return jobPreset; } } diff --git a/packages/core/src/employee-job-preset/commands/handlers/index.ts b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/index.ts similarity index 92% rename from packages/core/src/employee-job-preset/commands/handlers/index.ts rename to packages/plugins/job-search/src/employee-job-preset/commands/handlers/index.ts index 323c32db861..9ae78fe0194 100644 --- a/packages/core/src/employee-job-preset/commands/handlers/index.ts +++ b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/index.ts @@ -3,7 +3,7 @@ import { SaveEmployeeCriterionHandler } from './save-employee-criterion.handler' import { SaveEmployeePresetHandler } from './save-employee-preset.handler'; import { SavePresetCriterionHandler } from './save-preset-criterion.handler'; -export const Handlers = [ +export const CommandHandlers = [ CreateJobPresetHandler, SavePresetCriterionHandler, SaveEmployeePresetHandler, diff --git a/packages/core/src/employee-job-preset/commands/handlers/save-employee-criterion.handler.ts b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/save-employee-criterion.handler.ts similarity index 71% rename from packages/core/src/employee-job-preset/commands/handlers/save-employee-criterion.handler.ts rename to packages/plugins/job-search/src/employee-job-preset/commands/handlers/save-employee-criterion.handler.ts index 7937a8373ef..efc927e31ee 100644 --- a/packages/core/src/employee-job-preset/commands/handlers/save-employee-criterion.handler.ts +++ b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/save-employee-criterion.handler.ts @@ -1,39 +1,38 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { GauzyAIService } from '@gauzy/integration-ai'; import { IMatchingCriterions } from '@gauzy/contracts'; -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { InjectRepository } from '@nestjs/typeorm'; -import { RequestContext } from '../../../core/context'; -import { Employee } from '../../../employee/employee.entity'; +import { RequestContext, TypeOrmEmployeeRepository } from '@gauzy/core'; import { EmployeeUpworkJobsSearchCriterion } from '../../employee-upwork-jobs-search-criterion.entity'; import { SaveEmployeeCriterionCommand } from '../save-employee-criterion.command'; -import { TypeOrmEmployeeRepository } from '../../../employee/repository/type-orm-employee.repository'; import { TypeOrmEmployeeUpworkJobsSearchCriterionRepository } from '../../../employee-job-preset/repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository'; @CommandHandler(SaveEmployeeCriterionCommand) export class SaveEmployeeCriterionHandler implements ICommandHandler { constructor( - @InjectRepository(Employee) private readonly typeOrmEmployeeRepository: TypeOrmEmployeeRepository, - - @InjectRepository(EmployeeUpworkJobsSearchCriterion) private readonly typeOrmEmployeeUpworkJobsSearchCriterionRepository: TypeOrmEmployeeUpworkJobsSearchCriterionRepository, - private readonly gauzyAIService: GauzyAIService ) { } - public async execute( - command: SaveEmployeeCriterionCommand - ): Promise { + /** + * + * @param command + * @returns + */ + public async execute(command: SaveEmployeeCriterionCommand): Promise { const { input } = command; + + // Set tenantId input.tenantId = RequestContext.currentTenantId(); + // Set organizationId if not provided in the input if (!input.organizationId) { - const user = RequestContext.currentUser(); - const employee = await this.typeOrmEmployeeRepository.findOneBy({ - id: input.employeeId || user.employeeId - }); - input.organizationId = employee.organizationId; + const employeeId = RequestContext.currentEmployeeId(); + if (employeeId) { + const employee = await this.typeOrmEmployeeRepository.findOneBy({ id: employeeId }); + input.organizationId = employee.organizationId; + } } const creation = new EmployeeUpworkJobsSearchCriterion(input); diff --git a/packages/plugins/job-search/src/employee-job-preset/commands/handlers/save-employee-preset.handler.ts b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/save-employee-preset.handler.ts new file mode 100644 index 00000000000..b510497e203 --- /dev/null +++ b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/save-employee-preset.handler.ts @@ -0,0 +1,69 @@ +import { In } from 'typeorm'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { GauzyAIService } from '@gauzy/integration-ai'; +import { TypeOrmEmployeeRepository } from '@gauzy/core'; +import { EmployeeUpworkJobsSearchCriterion } from '../../employee-upwork-jobs-search-criterion.entity'; +import { JobPreset } from '../../job-preset.entity'; +import { SaveEmployeePresetCommand } from '../save-employee-preset.command'; +import { TypeOrmJobPresetRepository } from '../../repository/type-orm-job-preset.repository'; +import { TypeOrmEmployeeUpworkJobsSearchCriterionRepository } from '../../repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository'; + +@CommandHandler(SaveEmployeePresetCommand) +export class SaveEmployeePresetHandler implements ICommandHandler { + + constructor( + private readonly _typeOrmJobPresetRepository: TypeOrmJobPresetRepository, + private readonly _typeOrmEmployeeRepository: TypeOrmEmployeeRepository, + private readonly _typeOrmEmployeeUpworkJobsSearchCriterionRepository: TypeOrmEmployeeUpworkJobsSearchCriterionRepository, + private readonly _gauzyAIService: GauzyAIService + ) { } + + /** + * Saves employee presets and syncs job search criteria. + * + * @param command The SaveEmployeePresetCommand object containing input data. + * @returns A Promise resolving to an array of JobPreset objects. + */ + public async execute(command: SaveEmployeePresetCommand): Promise { + const { input } = command; + const { employeeId } = input; + + // Find the employee with related data + let employee = await this._typeOrmEmployeeRepository.findOne({ + where: { id: employeeId }, + relations: ['user', 'organization', 'customFields.jobPresets'] + }); + + // Find the job preset with related criteria + const jobPreset = await this._typeOrmJobPresetRepository.findOne({ + where: { id: In(input.jobPresetIds) }, + relations: { jobPresetCriterions: true } + }); + + // Map job preset criteria to employee criterions + const employeeCriterions = jobPreset.jobPresetCriterions.map(item => + new EmployeeUpworkJobsSearchCriterion({ ...item, employeeId }) + ); + + // Update employee custom fields with job presets + employee.customFields['jobPresets'] = input.jobPresetIds.map((id) => new JobPreset({ id })); + await this._typeOrmEmployeeRepository.save(employee); + + // Delete existing employee job search criteria + await this._typeOrmEmployeeUpworkJobsSearchCriterionRepository.delete({ employeeId }); + + // Save new employee job search criteria + await this._typeOrmEmployeeUpworkJobsSearchCriterionRepository.save(employeeCriterions); + + // Sync Gauzy employee job search criteria + this._gauzyAIService.syncGauzyEmployeeJobSearchCriteria(employee, employeeCriterions); + + // Find the employee with related data + employee = await this._typeOrmEmployeeRepository.findOne({ + where: { id: employeeId }, + relations: ['customFields.jobPresets'] + }); + + return employee.customFields['jobPresets']; + } +} diff --git a/packages/core/src/employee-job-preset/commands/handlers/save-preset-criterion.handler.ts b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/save-preset-criterion.handler.ts similarity index 52% rename from packages/core/src/employee-job-preset/commands/handlers/save-preset-criterion.handler.ts rename to packages/plugins/job-search/src/employee-job-preset/commands/handlers/save-preset-criterion.handler.ts index 4fd9cc4d9cc..eddda98af43 100644 --- a/packages/core/src/employee-job-preset/commands/handlers/save-preset-criterion.handler.ts +++ b/packages/plugins/job-search/src/employee-job-preset/commands/handlers/save-preset-criterion.handler.ts @@ -1,41 +1,44 @@ import { IMatchingCriterions } from '@gauzy/contracts'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { InjectRepository } from '@nestjs/typeorm'; -import { RequestContext } from '../../../core/context'; -import { Employee } from '../../../employee/employee.entity'; +import { RequestContext, TypeOrmEmployeeRepository } from '@gauzy/core'; import { JobPresetUpworkJobSearchCriterion } from '../../job-preset-upwork-job-search-criterion.entity'; import { SavePresetCriterionCommand } from '../save-preset-criterion.command'; import { TypeOrmJobPresetUpworkJobSearchCriterionRepository } from '../../repository/type-orm-job-preset-upwork-job-search-criterion.repository'; -import { TypeOrmEmployeeRepository } from '../../../employee/repository/type-orm-employee.repository'; @CommandHandler(SavePresetCriterionCommand) export class SavePresetCriterionHandler implements ICommandHandler { constructor( - @InjectRepository(Employee) private readonly typeOrmEmployeeRepository: TypeOrmEmployeeRepository, - - @InjectRepository(JobPresetUpworkJobSearchCriterion) private readonly typeOrmJobPresetUpworkJobSearchCriterionRepository: TypeOrmJobPresetUpworkJobSearchCriterionRepository ) { } - public async execute( - command: SavePresetCriterionCommand - ): Promise { + /** + * Executes the SavePresetCriterionCommand to save a preset criterion. + * + * @param command The command containing the input data for saving the preset criterion. + * @returns The saved preset criterion. + */ + public async execute(command: SavePresetCriterionCommand): Promise { const { input } = command; - const tenantId = RequestContext.currentTenantId(); + input.tenantId = RequestContext.currentTenantId(); + // If organizationId is not provided in the input, retrieve it from the current user's employee data if (!input.organizationId) { - const user = RequestContext.currentUser(); - const employee = await this.typeOrmEmployeeRepository.findOneBy({ - id: user.employeeId - }); - input.organizationId = employee.organizationId; + const employeeId = RequestContext.currentEmployeeId(); + if (employeeId) { + const employee = await this.typeOrmEmployeeRepository.findOneBy({ id: employeeId }); + input.organizationId = employee.organizationId; + } } - input.tenantId = tenantId; + // Create a new JobPresetUpworkJobSearchCriterion instance with the input data const creation = new JobPresetUpworkJobSearchCriterion(input); + + // Save the created instance to the database await this.typeOrmJobPresetUpworkJobSearchCriterionRepository.save(creation); + + // Return the saved preset criterion return creation; } } diff --git a/packages/core/src/employee-job-preset/commands/index.ts b/packages/plugins/job-search/src/employee-job-preset/commands/index.ts similarity index 100% rename from packages/core/src/employee-job-preset/commands/index.ts rename to packages/plugins/job-search/src/employee-job-preset/commands/index.ts diff --git a/packages/core/src/employee-job-preset/commands/save-employee-criterion.command.ts b/packages/plugins/job-search/src/employee-job-preset/commands/save-employee-criterion.command.ts similarity index 100% rename from packages/core/src/employee-job-preset/commands/save-employee-criterion.command.ts rename to packages/plugins/job-search/src/employee-job-preset/commands/save-employee-criterion.command.ts diff --git a/packages/core/src/employee-job-preset/commands/save-employee-preset.command.ts b/packages/plugins/job-search/src/employee-job-preset/commands/save-employee-preset.command.ts similarity index 100% rename from packages/core/src/employee-job-preset/commands/save-employee-preset.command.ts rename to packages/plugins/job-search/src/employee-job-preset/commands/save-employee-preset.command.ts diff --git a/packages/core/src/employee-job-preset/commands/save-preset-criterion.command.ts b/packages/plugins/job-search/src/employee-job-preset/commands/save-preset-criterion.command.ts similarity index 100% rename from packages/core/src/employee-job-preset/commands/save-preset-criterion.command.ts rename to packages/plugins/job-search/src/employee-job-preset/commands/save-preset-criterion.command.ts diff --git a/packages/core/src/employee-job-preset/dto/index.ts b/packages/plugins/job-search/src/employee-job-preset/dto/index.ts similarity index 100% rename from packages/core/src/employee-job-preset/dto/index.ts rename to packages/plugins/job-search/src/employee-job-preset/dto/index.ts diff --git a/packages/plugins/job-search/src/employee-job-preset/dto/job-preset-query.dto.ts b/packages/plugins/job-search/src/employee-job-preset/dto/job-preset-query.dto.ts new file mode 100644 index 00000000000..2a624bd10d5 --- /dev/null +++ b/packages/plugins/job-search/src/employee-job-preset/dto/job-preset-query.dto.ts @@ -0,0 +1,8 @@ +import { IntersectionType, PartialType, PickType } from '@nestjs/swagger'; +import { IGetJobPresetInput } from '@gauzy/contracts'; +import { EmployeeFeatureDTO, TenantOrganizationBaseDTO } from '@gauzy/core'; + +export class JobPresetQueryDTO extends IntersectionType( + TenantOrganizationBaseDTO, + PartialType(PickType(EmployeeFeatureDTO, ['employeeId'])) +) implements IGetJobPresetInput { } diff --git a/packages/core/src/employee-job-preset/employee-job-preset.module.ts b/packages/plugins/job-search/src/employee-job-preset/employee-job-preset.module.ts similarity index 50% rename from packages/core/src/employee-job-preset/employee-job-preset.module.ts rename to packages/plugins/job-search/src/employee-job-preset/employee-job-preset.module.ts index 1b667b49269..9b9cb5c432f 100644 --- a/packages/core/src/employee-job-preset/employee-job-preset.module.ts +++ b/packages/plugins/job-search/src/employee-job-preset/employee-job-preset.module.ts @@ -3,8 +3,9 @@ import { CqrsModule } from '@nestjs/cqrs'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RouterModule } from '@nestjs/core'; import { GauzyAIModule } from '@gauzy/integration-ai'; -import { EmployeeModule } from './../employee/employee.module'; -import { Handlers } from './commands/handlers'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { EmployeeModule } from '@gauzy/core'; +import { CommandHandlers } from './commands/handlers'; import { EmployeePresetController } from './employee-preset.controller'; import { EmployeeUpworkJobsSearchCriterion } from './employee-upwork-jobs-search-criterion.entity'; import { JobPresetUpworkJobSearchCriterion } from './job-preset-upwork-job-search-criterion.entity'; @@ -13,29 +14,31 @@ import { JobPresetService } from './job-preset.service'; import { JobSearchCategoryController } from './job-search-category/job-search-category.controller'; import { JobSearchCategory } from './job-search-category/job-search-category.entity'; import { JobSearchCategoryService } from './job-search-category/job-search-category.service'; +import { TypeOrmJobSearchCategoryRepository } from './job-search-category/repository/type-orm-job-search-category.repository'; import { JobSearchOccupationController } from './job-search-occupation/job-search-occupation.controller'; import { JobSearchOccupation } from './job-search-occupation/job-search-occupation.entity'; import { JobSearchOccupationService } from './job-search-occupation/job-search-occupation.service'; +import { TypeOrmJobSearchOccupationRepository } from './job-search-occupation/repository/type-orm-job-search-occupation.repository'; import { JobSearchPresetController } from './job-search-preset.controller'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { TypeOrmJobPresetRepository } from './repository/type-orm-job-preset.repository'; +import { TypeOrmEmployeeUpworkJobsSearchCriterionRepository } from './repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository'; +import { TypeOrmJobPresetUpworkJobSearchCriterionRepository } from './repository/type-orm-job-preset-upwork-job-search-criterion.repository'; + +export const entities = [ + JobPreset, + JobPresetUpworkJobSearchCriterion, + EmployeeUpworkJobsSearchCriterion, + JobSearchOccupation, + JobSearchCategory +]; @Module({ imports: [ - RouterModule.register([{ path: '/job-preset', module: EmployeeJobPresetModule }]), - TypeOrmModule.forFeature([ - JobPreset, - JobPresetUpworkJobSearchCriterion, - EmployeeUpworkJobsSearchCriterion, - JobSearchOccupation, - JobSearchCategory - ]), - MikroOrmModule.forFeature([ - JobPreset, - JobPresetUpworkJobSearchCriterion, - EmployeeUpworkJobsSearchCriterion, - JobSearchOccupation, - JobSearchCategory + RouterModule.register([ + { path: '/job-preset', module: EmployeeJobPresetModule } ]), + TypeOrmModule.forFeature([...entities]), + MikroOrmModule.forFeature([...entities]), EmployeeModule, CqrsModule, GauzyAIModule.forRoot() @@ -46,7 +49,23 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; EmployeePresetController, JobSearchPresetController ], - providers: [...Handlers, JobPresetService, JobSearchCategoryService, JobSearchOccupationService], - exports: [JobPresetService, JobSearchCategoryService, JobSearchOccupationService] + providers: [ + ...CommandHandlers, + JobPresetService, + JobSearchCategoryService, + JobSearchOccupationService, + TypeOrmJobPresetRepository, + TypeOrmJobSearchCategoryRepository, + TypeOrmJobSearchOccupationRepository, + TypeOrmJobPresetUpworkJobSearchCriterionRepository, + TypeOrmEmployeeUpworkJobsSearchCriterionRepository + ], + exports: [ + TypeOrmModule, + MikroOrmModule, + JobPresetService, + JobSearchCategoryService, + JobSearchOccupationService + ] }) export class EmployeeJobPresetModule { } diff --git a/packages/core/src/employee-job-preset/employee-preset.controller.ts b/packages/plugins/job-search/src/employee-job-preset/employee-preset.controller.ts similarity index 56% rename from packages/core/src/employee-job-preset/employee-preset.controller.ts rename to packages/plugins/job-search/src/employee-job-preset/employee-preset.controller.ts index fd958a21bbb..e6b0eb71477 100644 --- a/packages/core/src/employee-job-preset/employee-preset.controller.ts +++ b/packages/plugins/job-search/src/employee-job-preset/employee-preset.controller.ts @@ -9,21 +9,33 @@ import { Delete } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { DeleteResult } from 'typeorm'; import { IEmployeePresetInput, + IEmployeeUpworkJobsSearchCriterion, IGetMatchingCriterions, + IJobPreset, IMatchingCriterions } from '@gauzy/contracts'; +import { UUIDValidationPipe } from '@gauzy/core'; import { JobPresetService } from './job-preset.service'; import { JobPreset } from './job-preset.entity'; -import { UUIDValidationPipe } from './../shared/pipes'; @ApiTags('EmployeeJobPreset') @Controller('employee') export class EmployeePresetController { - constructor(private readonly jobPresetService: JobPresetService) {} - @ApiOperation({ summary: 'Save Employee preset' }) + constructor( + private readonly jobPresetService: JobPresetService + ) { } + + /** + * Retrieves the job preset for a specific employee. + * + * @param employeeId The ID of the employee. + * @returns The job preset for the specified employee. + */ + @ApiOperation({ summary: 'Retrieves the job preset for a specific employee.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Found employee job preset', @@ -36,14 +48,21 @@ export class EmployeePresetController { @Get(':employeeId') async getEmployeePreset( @Param('employeeId', UUIDValidationPipe) employeeId: string - ) { + ): Promise { return await this.jobPresetService.getEmployeePreset(employeeId); } + /** + * Retrieves all matching criteria for job presets of a specific employee. + * + * @param employeeId The ID of the employee. + * @param request The request containing criteria for matching. + * @returns The matching criteria for job presets of the specified employee. + */ @ApiOperation({ summary: 'Find all employee job posts' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found employee job preset', + description: 'Found matching criteria for employee job presets', type: JobPreset }) @ApiResponse({ @@ -54,17 +73,24 @@ export class EmployeePresetController { async getEmployeeCriterion( @Param('employeeId', UUIDValidationPipe) employeeId: string, @Query() request: IGetMatchingCriterions - ) { + ): Promise { return await this.jobPresetService.getEmployeeCriterion({ ...request, employeeId }); } - @ApiOperation({ summary: 'Find all employee job posts' }) + /** + * Saves or updates matching criteria for job presets of a specific employee. + * + * @param employeeId The ID of the employee. + * @param request The request containing criteria for matching. + * @returns The saved or updated job presets for the specified employee. + */ + @ApiOperation({ summary: 'Save or update employee job presets' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found employee job preset', + description: 'Employee job presets saved or updated successfully', type: JobPreset }) @ApiResponse({ @@ -75,17 +101,23 @@ export class EmployeePresetController { async saveUpdateEmployeeCriterion( @Param('employeeId', UUIDValidationPipe) employeeId: string, @Body() request: IMatchingCriterions - ) { + ): Promise { return await this.jobPresetService.saveEmployeeCriterion({ ...request, employeeId }); } + /** + * Saves an employee preset. + * + * @param request The request containing the employee preset data. + * @returns The saved employee job preset. + */ @ApiOperation({ summary: 'Save Employee preset' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found employee job preset', + description: 'Employee job preset saved successfully', type: JobPreset }) @ApiResponse({ @@ -95,14 +127,21 @@ export class EmployeePresetController { @Post() async saveEmployeePreset( @Body() request: IEmployeePresetInput - ) { + ): Promise { return await this.jobPresetService.saveEmployeePreset(request); } - @ApiOperation({ summary: 'Find all employee job posts' }) + /** + * Deletes an employee job preset criterion. + * + * @param criterionId The ID of the criterion to delete. + * @param employeeId The ID of the employee whose criterion to delete. + * @returns The deleted employee job preset. + */ + @ApiOperation({ summary: 'Delete employee job preset criterion' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found employee job preset', + description: 'Employee job preset criterion deleted successfully', type: JobPreset }) @ApiResponse({ @@ -113,7 +152,7 @@ export class EmployeePresetController { async deleteEmployeeCriterion( @Param('criterionId', UUIDValidationPipe) criterionId: string, @Param('employeeId', UUIDValidationPipe) employeeId: string - ) { + ): Promise { return await this.jobPresetService.deleteEmployeeCriterion( criterionId, employeeId diff --git a/packages/core/src/employee-job-preset/employee-upwork-jobs-search-criterion.entity.ts b/packages/plugins/job-search/src/employee-job-preset/employee-upwork-jobs-search-criterion.entity.ts similarity index 70% rename from packages/core/src/employee-job-preset/employee-upwork-jobs-search-criterion.entity.ts rename to packages/plugins/job-search/src/employee-job-preset/employee-upwork-jobs-search-criterion.entity.ts index 0deac188f46..a2996cbddb5 100644 --- a/packages/core/src/employee-job-preset/employee-upwork-jobs-search-criterion.entity.ts +++ b/packages/plugins/job-search/src/employee-job-preset/employee-upwork-jobs-search-criterion.entity.ts @@ -1,4 +1,4 @@ -import { RelationId } from 'typeorm'; +import { DeepPartial, JoinColumn, RelationId } from 'typeorm'; import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; import { @@ -9,19 +9,19 @@ import { IJobSearchOccupation, JobPostTypeEnum } from '@gauzy/contracts'; -import { - Employee, - JobPreset, - JobSearchCategory, - JobSearchOccupation, - TenantOrganizationBaseEntity -} from '../core/entities/internal'; -import { MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from './../core/decorators/entity'; +import { Employee, MultiORMColumn, MultiORMEntity, MultiORMManyToOne, TenantOrganizationBaseEntity } from '@gauzy/core'; +import { JobPreset } from './job-preset.entity'; +import { JobSearchOccupation } from './job-search-occupation/job-search-occupation.entity'; +import { JobSearchCategory } from './job-search-category/job-search-category.entity'; import { MikroOrmEmployeeUpworkJobsSearchCriterionRepository } from './repository/mikro-orm-employee-upwork-jobs-search-criterion.entity.repository'; @MultiORMEntity('employee_upwork_job_search_criterion', { mikroOrmRepository: () => MikroOrmEmployeeUpworkJobsSearchCriterionRepository }) export class EmployeeUpworkJobsSearchCriterion extends TenantOrganizationBaseEntity implements IEmployeeUpworkJobsSearchCriterion { + constructor(input?: DeepPartial) { + super(input); + } + @ApiProperty({ type: () => String }) @IsString() @IsNotEmpty() @@ -43,7 +43,11 @@ export class EmployeeUpworkJobsSearchCriterion extends TenantOrganizationBaseEnt /** * */ - @MultiORMManyToOne(() => JobPreset, (it) => it.employeeCriterions) + @MultiORMManyToOne(() => JobPreset, (it) => it.employeeCriterions, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true + }) + @JoinColumn() jobPreset?: IJobPreset; @ApiProperty({ type: () => String }) @@ -57,6 +61,7 @@ export class EmployeeUpworkJobsSearchCriterion extends TenantOrganizationBaseEnt * */ @MultiORMManyToOne(() => Employee) + @JoinColumn() employee?: IEmployee; @ApiProperty({ type: () => String }) @@ -69,7 +74,11 @@ export class EmployeeUpworkJobsSearchCriterion extends TenantOrganizationBaseEnt /** * */ - @MultiORMManyToOne(() => JobSearchOccupation, (it) => it.employeeCriterions) + @MultiORMManyToOne(() => JobSearchOccupation, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true + }) + @JoinColumn() occupation?: IJobSearchOccupation; @ApiProperty({ type: () => String }) @@ -82,7 +91,11 @@ export class EmployeeUpworkJobsSearchCriterion extends TenantOrganizationBaseEnt /** * */ - @MultiORMManyToOne(() => JobSearchCategory, (it) => it.employeeCriterions) + @MultiORMManyToOne(() => JobSearchCategory, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true + }) + @JoinColumn() category?: IJobSearchCategory; @ApiProperty({ type: () => String }) diff --git a/packages/core/src/employee-job-preset/job-preset-upwork-job-search-criterion.entity.ts b/packages/plugins/job-search/src/employee-job-preset/job-preset-upwork-job-search-criterion.entity.ts similarity index 68% rename from packages/core/src/employee-job-preset/job-preset-upwork-job-search-criterion.entity.ts rename to packages/plugins/job-search/src/employee-job-preset/job-preset-upwork-job-search-criterion.entity.ts index 38708fb61a2..655ea292869 100644 --- a/packages/core/src/employee-job-preset/job-preset-upwork-job-search-criterion.entity.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-preset-upwork-job-search-criterion.entity.ts @@ -1,4 +1,4 @@ -import { RelationId } from 'typeorm'; +import { DeepPartial, JoinColumn, RelationId } from 'typeorm'; import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString, IsUUID } from 'class-validator'; import { @@ -8,18 +8,19 @@ import { IJobSearchOccupation, JobPostTypeEnum } from '@gauzy/contracts'; -import { - JobPreset, - JobSearchCategory, - JobSearchOccupation, - TenantOrganizationBaseEntity -} from '../core/entities/internal'; -import { MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from './../core/decorators/entity'; +import { MultiORMColumn, MultiORMEntity, MultiORMManyToOne, TenantOrganizationBaseEntity } from '@gauzy/core'; import { MikroOrmJobPresetUpworkJobSearchCriterionRepository } from './repository/mikro-orm-job-preset-upwork-job-search-criterion.repository'; +import { JobPreset } from './job-preset.entity'; +import { JobSearchOccupation } from './job-search-occupation/job-search-occupation.entity'; +import { JobSearchCategory } from './job-search-category/job-search-category.entity'; @MultiORMEntity('job_preset_upwork_job_search_criterion', { mikroOrmRepository: () => MikroOrmJobPresetUpworkJobSearchCriterionRepository }) export class JobPresetUpworkJobSearchCriterion extends TenantOrganizationBaseEntity implements IJobPresetUpworkJobSearchCriterion { + constructor(input?: DeepPartial) { + super(input); + } + @ApiProperty({ type: () => String }) @IsNotEmpty() @IsString() @@ -41,7 +42,11 @@ export class JobPresetUpworkJobSearchCriterion extends TenantOrganizationBaseEnt /** * */ - @MultiORMManyToOne(() => JobPreset, (it) => it.jobPresetCriterions) + @MultiORMManyToOne(() => JobPreset, (it) => it.jobPresetCriterions, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true + }) + @JoinColumn() jobPreset?: IJobPreset; @ApiProperty({ type: () => String }) @@ -54,7 +59,11 @@ export class JobPresetUpworkJobSearchCriterion extends TenantOrganizationBaseEnt /** * */ - @MultiORMManyToOne(() => JobSearchOccupation, (it) => it.jobPresetCriterions) + @MultiORMManyToOne(() => JobSearchOccupation, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true + }) + @JoinColumn() occupation?: IJobSearchOccupation; @ApiProperty({ type: () => String }) @@ -67,7 +76,11 @@ export class JobPresetUpworkJobSearchCriterion extends TenantOrganizationBaseEnt /** * */ - @MultiORMManyToOne(() => JobSearchCategory, (it) => it.jobPresetCriterions) + @MultiORMManyToOne(() => JobSearchCategory, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true + }) + @JoinColumn() category?: IJobSearchCategory; @ApiProperty({ type: () => String }) diff --git a/packages/core/src/employee-job-preset/job-preset.entity.ts b/packages/plugins/job-search/src/employee-job-preset/job-preset.entity.ts similarity index 61% rename from packages/core/src/employee-job-preset/job-preset.entity.ts rename to packages/plugins/job-search/src/employee-job-preset/job-preset.entity.ts index 6cf2dda0792..279c40d1d97 100644 --- a/packages/core/src/employee-job-preset/job-preset.entity.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-preset.entity.ts @@ -1,25 +1,24 @@ -import { - JoinTable -} from 'typeorm'; import { ApiProperty } from '@nestjs/swagger'; +import { DeepPartial, JoinTable } from 'typeorm'; import { IsNotEmpty, IsString } from 'class-validator'; import { IEmployeeUpworkJobsSearchCriterion, IJobPresetUpworkJobSearchCriterion, IJobPreset } from '@gauzy/contracts'; -import { - Employee, - EmployeeUpworkJobsSearchCriterion, - JobPresetUpworkJobSearchCriterion, - TenantOrganizationBaseEntity -} from '../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToMany, MultiORMOneToMany } from './../core/decorators/entity'; +import { Employee, TenantOrganizationBaseEntity } from '@gauzy/core'; +import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToMany, MultiORMOneToMany } from '@gauzy/core'; import { MikroOrmJobPresetRepository } from './repository/mikro-orm-job-preset.repository'; +import { EmployeeUpworkJobsSearchCriterion } from './employee-upwork-jobs-search-criterion.entity'; +import { JobPresetUpworkJobSearchCriterion } from './job-preset-upwork-job-search-criterion.entity'; @MultiORMEntity('job_preset', { mikroOrmRepository: () => MikroOrmJobPresetRepository }) export class JobPreset extends TenantOrganizationBaseEntity implements IJobPreset { + constructor(input?: DeepPartial) { + super(input); + } + @ApiProperty({ type: () => String }) @IsString() @IsNotEmpty() @@ -35,7 +34,7 @@ export class JobPreset extends TenantOrganizationBaseEntity implements IJobPrese /** * Employee Job Criterions */ - @MultiORMOneToMany(() => EmployeeUpworkJobsSearchCriterion, (it) => it.jobPreset, { + @MultiORMOneToMany(() => EmployeeUpworkJobsSearchCriterion, (it: EmployeeUpworkJobsSearchCriterion) => it.jobPreset, { onDelete: 'CASCADE' }) employeeCriterions?: IEmployeeUpworkJobsSearchCriterion[]; @@ -43,7 +42,7 @@ export class JobPreset extends TenantOrganizationBaseEntity implements IJobPrese /** * Job Criterions */ - @MultiORMOneToMany(() => JobPresetUpworkJobSearchCriterion, (it) => it.jobPreset, { + @MultiORMOneToMany(() => JobPresetUpworkJobSearchCriterion, (it: JobPresetUpworkJobSearchCriterion) => it.jobPreset, { onDelete: 'CASCADE' }) jobPresetCriterions?: IJobPresetUpworkJobSearchCriterion[]; @@ -57,15 +56,17 @@ export class JobPreset extends TenantOrganizationBaseEntity implements IJobPrese /** * Job Preset Employees */ - @MultiORMManyToMany(() => Employee, (it) => it.jobPresets, { + @MultiORMManyToMany(() => Employee, (it: Employee) => it.customFields['jobPresets'], { cascade: true, + /** This column is a boolean flag indicating whether the current entity is the 'owning' side of a relationship. */ owner: true, + /** Pivot table for many-to-many relationship. */ pivotTable: 'employee_job_preset', + /** Column in pivot table referencing 'time_slot' primary key. */ joinColumn: 'jobPresetId', + /** Column in pivot table referencing 'time_logs' primary key. */ inverseJoinColumn: 'employeeId', }) - @JoinTable({ - name: 'employee_job_preset' - }) + @JoinTable({ name: 'employee_job_preset' }) employees?: Employee[]; } diff --git a/packages/plugins/job-search/src/employee-job-preset/job-preset.service.ts b/packages/plugins/job-search/src/employee-job-preset/job-preset.service.ts new file mode 100644 index 00000000000..b0bbc3a24e8 --- /dev/null +++ b/packages/plugins/job-search/src/employee-job-preset/job-preset.service.ts @@ -0,0 +1,234 @@ +import { Injectable } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { DeleteResult, SelectQueryBuilder } from 'typeorm'; +import { isNotEmpty } from 'class-validator'; +import { + IEmployeePresetInput, + IGetJobPresetCriterionInput, + IGetJobPresetInput, + IGetMatchingCriterions, + IJobPreset, + IMatchingCriterions +} from '@gauzy/contracts'; +import { isPostgres } from '@gauzy/config'; +import { RequestContext, TenantAwareCrudService, TypeOrmEmployeeRepository } from '@gauzy/core'; +import { prepareSQLQuery as p } from '@gauzy/core'; +import { JobPreset } from './job-preset.entity'; +import { + CreateJobPresetCommand, + SaveEmployeeCriterionCommand, + SaveEmployeePresetCommand, + SavePresetCriterionCommand +} from './commands'; +import { TypeOrmJobPresetRepository } from './repository/type-orm-job-preset.repository'; +import { MikroOrmJobPresetRepository } from './repository/mikro-orm-job-preset.repository'; +import { TypeOrmJobPresetUpworkJobSearchCriterionRepository } from './repository/type-orm-job-preset-upwork-job-search-criterion.repository'; +import { TypeOrmEmployeeUpworkJobsSearchCriterionRepository } from './repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository'; + +@Injectable() +export class JobPresetService extends TenantAwareCrudService { + constructor( + private readonly typeOrmJobPresetRepository: TypeOrmJobPresetRepository, + private readonly mikroOrmJobPresetRepository: MikroOrmJobPresetRepository, + private readonly typeOrmJobPresetUpworkJobSearchCriterionRepository: TypeOrmJobPresetUpworkJobSearchCriterionRepository, + private readonly typeOrmEmployeeUpworkJobsSearchCriterionRepository: TypeOrmEmployeeUpworkJobsSearchCriterionRepository, + private readonly typeOrmEmployeeRepository: TypeOrmEmployeeRepository, + private readonly commandBus: CommandBus, + ) { + super(typeOrmJobPresetRepository, mikroOrmJobPresetRepository); + } + + /** + * Retrieves all job presets optionally filtered by tenant ID, organization ID, search string, or employee ID. + * + * @param request Additional parameters for filtering the job presets. + * @returns A Promise that resolves to an array of job presets. + */ + public async getAll(request?: IGetJobPresetInput) { + // Extract parameters from the request object + const { tenantId, organizationId, search, employeeId } = request || {}; + // Determine the appropriate LIKE operator based on the database type + const likeOperator = isPostgres() ? 'ILIKE' : 'LIKE'; + + // Create a query builder for the JobPreset entity + const query = this.typeOrmRepository.createQueryBuilder('job_preset'); + + // Set the find options for the query + query.setFindOptions({ + join: { + alias: 'job_preset', + // Left join employees relation + leftJoin: { employees: 'job_preset.employees' } + }, + // Include job preset criterions in the query result + relations: { jobPresetCriterions: true }, + // Order the results by job preset name in ascending order + order: { name: 'ASC' } + }); + + // Add conditions to the query using the query builder + query.where((qb: SelectQueryBuilder) => { + // Filter by tenant ID + qb.andWhere(p(`"${qb.alias}"."tenantId" = :tenantId`), { tenantId: tenantId || RequestContext.currentTenantId() }); + + // Filter by organization ID if provided + if (isNotEmpty(organizationId)) { + qb.andWhere(p(`"${qb.alias}"."organizationId" = :organizationId`), { organizationId }); + } + + // Filter by search string if provided + if (isNotEmpty(search)) { + qb.andWhere(p(`"${query.alias}"."name" ${likeOperator} :search`), { search: `%${search}%` }); + } + + // Filter by employee ID if provided + if (isNotEmpty(employeeId)) { + qb.andWhere(p(`"employees"."id" = :employeeId`), { employeeId }); + } + }); + + // Execute the query and return the result + return await query.getMany(); + } + + /** + * Retrieves a job preset by its ID along with its job preset criteria and employee criteria if requested. + * + * @param id The ID of the job preset to retrieve. + * @param request Additional parameters for the query, such as employeeId for fetching employee criteria. + * @returns A Promise that resolves to the retrieved job preset. + */ + public async get(id: string, request?: IGetJobPresetCriterionInput) { + const query = this.typeOrmRepository.createQueryBuilder(); + + // Left join job preset criterions + query.leftJoinAndSelect(`${query.alias}.jobPresetCriterions`, 'jobPresetCriterions'); + + // Left join employee criterions if employeeId is provided in the request + if (request?.employeeId) { + const { employeeId } = request; + query.leftJoinAndSelect( + `${query.alias}.employeeCriterions`, + 'employeeCriterions', + 'employeeCriterions.employeeId = :employeeId', + { employeeId } + ); + } + + // Filter by job preset ID + query.andWhere(`${query.alias}.id = :id`, { id }); + + // Execute the query and return the result + return await query.getOne(); + } + + /** + * Retrieves job preset criterion based on the preset ID. + * @param presetId The ID of the job preset. + * @returns A Promise that resolves to an array of job preset criterion. + */ + public async getJobPresetCriterion(presetId: string) { + // Use the job preset ID to find related job preset criterion + return await this.typeOrmJobPresetUpworkJobSearchCriterionRepository.findBy({ jobPresetId: presetId }); + } + + + /** + * Retrieves employee criteria based on the provided input. + * @param input The input data for retrieving employee criteria. + * @returns A Promise that resolves to the employee criteria matching the input. + */ + public async getEmployeeCriterion(input: IGetMatchingCriterions) { + return await this.typeOrmEmployeeUpworkJobsSearchCriterionRepository.findBy({ + ...(input.jobPresetId ? { jobPresetId: input.jobPresetId } : {}), + employeeId: input.employeeId + }); + } + + /** + * Creates a new job preset using the provided request data. + * @param request The request data for creating the job preset. + * @returns A Promise that resolves to the created job preset. + */ + public async createJobPreset(request: IJobPreset) { + return await this.commandBus.execute( + new CreateJobPresetCommand(request) + ); + } + + /** + * Saves job preset criterion based on the provided criteria. + * @param request The criteria for saving job preset criterion. + * @returns A Promise that resolves to the result of the command execution. + */ + async saveJobPresetCriterion(request: IMatchingCriterions) { + // Execute the SavePresetCriterionCommand with the provided criteria + return this.commandBus.execute( + new SavePresetCriterionCommand(request) + ); + } + + /** + * Saves employee criterion based on the provided criteria. + * @param request The criteria for saving employee criterion. + * @returns A Promise that resolves to the result of the command execution. + */ + async saveEmployeeCriterion(request: IMatchingCriterions) { + // Execute the SaveEmployeeCriterionCommand with the provided criteria + return this.commandBus.execute( + new SaveEmployeeCriterionCommand(request) + ); + } + + /** + * Retrieves the job presets associated with the specified employee. + * @param employeeId The ID of the employee. + * @returns A Promise that resolves to the job presets associated with the employee. + */ + async getEmployeePreset(employeeId: string): Promise { + // Find the employee with the specified ID and include jobPresets relation + const employee = await this.typeOrmEmployeeRepository.findOne({ + where: { id: employeeId }, + relations: ['customFields.jobPresets'] + }); + + // Return the job presets associated with the employee + return employee.customFields['jobPresets']; + } + + /** + * Saves employee presets based on the provided input. + * @param request The input containing employee presets to be saved. + * @returns A Promise that resolves to the result of the command execution. + */ + async saveEmployeePreset(request: IEmployeePresetInput): Promise { + // Execute the SaveEmployeePresetCommand with the provided input + return await this.commandBus.execute( + new SaveEmployeePresetCommand(request) + ); + } + + /** + * Deletes the employee criterion with the specified ID associated with the employee ID. + * @param creationId The ID of the employee criterion to be deleted. + * @param employeeId The ID of the employee. + * @returns A Promise that resolves to the result of the deletion operation. + */ + async deleteEmployeeCriterion(creationId: string, employeeId: string): Promise { + // Delete the employee criterion with the specified ID associated with the employee ID + return await this.typeOrmEmployeeUpworkJobsSearchCriterionRepository.delete({ + id: creationId, + employeeId: employeeId + }); + } + + /** + * Deletes the job preset criterion with the specified ID. + * @param creationId The ID of the job preset criterion to be deleted. + * @returns A Promise that resolves to the result of the deletion operation. + */ + async deleteJobPresetCriterion(creationId: string) { + // Delete the job preset criterion with the specified ID + return await this.typeOrmJobPresetUpworkJobSearchCriterionRepository.delete(creationId); + } +} diff --git a/packages/core/src/employee-job-preset/job-search-category/job-search-category.controller.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.controller.ts similarity index 75% rename from packages/core/src/employee-job-preset/job-search-category/job-search-category.controller.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.controller.ts index 36a7523ff27..b9157eb0344 100644 --- a/packages/core/src/employee-job-preset/job-search-category/job-search-category.controller.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.controller.ts @@ -1,13 +1,13 @@ import { Controller } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { CrudController } from '../../core/crud'; +import { CrudController } from '@gauzy/core'; import { JobSearchCategory } from './job-search-category.entity'; import { JobSearchCategoryService } from './job-search-category.service'; @ApiTags('JobSearchCategory') @Controller('job-search-category') export class JobSearchCategoryController extends CrudController { - constructor(private readonly jobSearchCategoryService: JobSearchCategoryService) { + constructor(protected readonly jobSearchCategoryService: JobSearchCategoryService) { super(jobSearchCategoryService); } } diff --git a/packages/core/src/employee-job-preset/job-search-category/job-search-category.entity.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.entity.ts similarity index 52% rename from packages/core/src/employee-job-preset/job-search-category/job-search-category.entity.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.entity.ts index 01a0a043e22..8fc4397ec7b 100644 --- a/packages/core/src/employee-job-preset/job-search-category/job-search-category.entity.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.entity.ts @@ -1,23 +1,18 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { DeepPartial } from 'typeorm'; import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; -import { - JobPostSourceEnum, - IEmployeeUpworkJobsSearchCriterion, - IJobPresetUpworkJobSearchCriterion, - IJobSearchCategory -} from '@gauzy/contracts'; +import { JobPostSourceEnum, IJobSearchCategory } from '@gauzy/contracts'; import { isMySQL } from '@gauzy/config'; -import { - EmployeeUpworkJobsSearchCriterion, - JobPresetUpworkJobSearchCriterion, - TenantOrganizationBaseEntity -} from '../../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMOneToMany } from './../../core/decorators/entity'; +import { ColumnIndex, MultiORMColumn, MultiORMEntity, TenantOrganizationBaseEntity } from '@gauzy/core'; import { MikroOrmJobSearchCategoryRepository } from './repository/mikro-orm-job-search-category.repository'; @MultiORMEntity('job_search_category', { mikroOrmRepository: () => MikroOrmJobSearchCategoryRepository }) export class JobSearchCategory extends TenantOrganizationBaseEntity implements IJobSearchCategory { + constructor(input?: DeepPartial) { + super(input); + } + @ApiProperty({ type: () => String }) @IsNotEmpty() @IsString() @@ -42,26 +37,4 @@ export class JobSearchCategory extends TenantOrganizationBaseEntity implements I ...(isMySQL() ? { type: 'enum', enum: JobPostSourceEnum } : { type: 'text' }) }) jobSource?: JobPostSourceEnum; - - /* - |-------------------------------------------------------------------------- - | @OneToMany - |-------------------------------------------------------------------------- - */ - - /** - * EmployeeUpworkJobsSearchCriterion - */ - @MultiORMOneToMany(() => EmployeeUpworkJobsSearchCriterion, (it) => it.category, { - onDelete: 'CASCADE' - }) - employeeCriterions?: IEmployeeUpworkJobsSearchCriterion[]; - - /** - * JobPresetUpworkJobSearchCriterion - */ - @MultiORMOneToMany(() => JobPresetUpworkJobSearchCriterion, (it) => it.category, { - onDelete: 'CASCADE' - }) - jobPresetCriterions?: IJobPresetUpworkJobSearchCriterion[]; } diff --git a/packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.seed.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.seed.ts new file mode 100644 index 00000000000..199d4a25f0e --- /dev/null +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.seed.ts @@ -0,0 +1,35 @@ +import { DataSource } from 'typeorm'; +import { IOrganization, ITenant, JobPostSourceEnum } from '@gauzy/contracts'; +import { JobSearchCategory } from './job-search-category.entity'; + +/** + * Creates default job search categories. + * + * @param connection The connection to the data source for database operations. + * @param tenant The tenant for which categories are created. + * @param organization The organization for which categories are created. + * @returns A Promise that resolves with the created job search categories. + */ +export const createDefaultJobSearchCategories = async ( + connection: DataSource, + tenant: ITenant, + organization: IOrganization +): Promise => { + const upworkCategories = [ + { name: 'IT & Networking', jobSourceCategoryId: '531770282580668419' }, + { name: 'Web, Mobile & Software Dev', jobSourceCategoryId: '531770282580668418' } + ]; + + const categories: JobSearchCategory[] = upworkCategories.map(category => { + const cat = new JobSearchCategory(); + cat.jobSource = JobPostSourceEnum.UPWORK; + cat.organizationId = organization.id; + cat.tenantId = tenant.id; + cat.name = category.name; + cat.jobSourceCategoryId = category.jobSourceCategoryId; + return cat; + }); + + await connection.manager.save(categories); + return categories; +}; diff --git a/packages/core/src/employee-job-preset/job-search-category/job-search-category.service.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.service.ts similarity index 56% rename from packages/core/src/employee-job-preset/job-search-category/job-search-category.service.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.service.ts index dbd8a813407..6f80613b29b 100644 --- a/packages/core/src/employee-job-preset/job-search-category/job-search-category.service.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-category/job-search-category.service.ts @@ -1,17 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { TenantAwareCrudService } from './../../core/crud'; +import { TenantAwareCrudService } from '@gauzy/core'; import { JobSearchCategory } from './job-search-category.entity'; -import { TypeOrmJobSearchCategoryRepository } from './repository/type-orm-job-search-category.repository'; -import { MikroOrmJobSearchCategoryRepository } from './repository/mikro-orm-job-search-category.repository'; +import { MikroOrmJobSearchCategoryRepository, TypeOrmJobSearchCategoryRepository } from './repository'; @Injectable() export class JobSearchCategoryService extends TenantAwareCrudService { - constructor( - @InjectRepository(JobSearchCategory) typeOrmJobSearchCategoryRepository: TypeOrmJobSearchCategoryRepository, - mikroOrmJobSearchCategoryRepository: MikroOrmJobSearchCategoryRepository ) { super(typeOrmJobSearchCategoryRepository, mikroOrmJobSearchCategoryRepository); diff --git a/packages/plugins/job-search/src/employee-job-preset/job-search-category/repository/index.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-category/repository/index.ts new file mode 100644 index 00000000000..b3de132c2c7 --- /dev/null +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-category/repository/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-job-search-category.repository'; +export * from './type-orm-job-search-category.repository'; diff --git a/packages/core/src/employee-job-preset/job-search-category/repository/mikro-orm-job-search-category.repository.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-category/repository/mikro-orm-job-search-category.repository.ts similarity index 62% rename from packages/core/src/employee-job-preset/job-search-category/repository/mikro-orm-job-search-category.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-category/repository/mikro-orm-job-search-category.repository.ts index 3ce9767894c..e6333fa0e44 100644 --- a/packages/core/src/employee-job-preset/job-search-category/repository/mikro-orm-job-search-category.repository.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-category/repository/mikro-orm-job-search-category.repository.ts @@ -1,4 +1,4 @@ -import { MikroOrmBaseEntityRepository } from '../../../core/repository/mikro-orm-base-entity.repository'; +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; import { JobSearchCategory } from '../job-search-category.entity'; export class MikroOrmJobSearchCategoryRepository extends MikroOrmBaseEntityRepository { } diff --git a/packages/core/src/employee-job-preset/job-search-category/repository/type-orm-job-search-category.repository.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-category/repository/type-orm-job-search-category.repository.ts similarity index 100% rename from packages/core/src/employee-job-preset/job-search-category/repository/type-orm-job-search-category.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-category/repository/type-orm-job-search-category.repository.ts diff --git a/packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.controller.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.controller.ts similarity index 76% rename from packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.controller.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.controller.ts index af47b77e164..c92456e995c 100644 --- a/packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.controller.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.controller.ts @@ -1,13 +1,13 @@ import { Controller } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { CrudController } from '@gauzy/core'; import { JobSearchOccupationService } from './job-search-occupation.service'; -import { CrudController } from '../../core/crud'; import { JobSearchOccupation } from './job-search-occupation.entity'; @ApiTags('JobSearchOccupation') @Controller('job-search-occupation') export class JobSearchOccupationController extends CrudController { - constructor(private readonly jobSearchOccupationService: JobSearchOccupationService) { + constructor(protected readonly jobSearchOccupationService: JobSearchOccupationService) { super(jobSearchOccupationService); } } diff --git a/packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.entity.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.entity.ts similarity index 50% rename from packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.entity.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.entity.ts index 20470628bd4..f7a6a2b2b7c 100644 --- a/packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.entity.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.entity.ts @@ -1,23 +1,18 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { DeepPartial } from 'typeorm'; import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; -import { - JobPostSourceEnum, - IJobSearchOccupation, - IJobPresetUpworkJobSearchCriterion, - IEmployeeUpworkJobsSearchCriterion -} from '@gauzy/contracts'; +import { JobPostSourceEnum, IJobSearchOccupation } from '@gauzy/contracts'; import { isMySQL } from "@gauzy/config"; -import { - EmployeeUpworkJobsSearchCriterion, - JobPresetUpworkJobSearchCriterion, - TenantOrganizationBaseEntity -} from '../../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMOneToMany } from './../../core/decorators/entity'; +import { ColumnIndex, MultiORMColumn, MultiORMEntity, TenantOrganizationBaseEntity } from '@gauzy/core'; import { MikroOrmJobSearchOccupationRepository } from './repository/mikro-orm-job-search-occupation.repository'; @MultiORMEntity('job_search_occupation', { mikroOrmRepository: () => MikroOrmJobSearchOccupationRepository }) export class JobSearchOccupation extends TenantOrganizationBaseEntity implements IJobSearchOccupation { + constructor(input?: DeepPartial) { + super(input); + } + @ApiProperty({ type: () => String }) @IsNotEmpty() @IsString() @@ -42,28 +37,4 @@ export class JobSearchOccupation extends TenantOrganizationBaseEntity implements ...(isMySQL() ? { type: 'enum', enum: JobPostSourceEnum } : { type: 'text' }) }) jobSource?: JobPostSourceEnum; - - /* - |-------------------------------------------------------------------------- - | @OneToMany - |-------------------------------------------------------------------------- - */ - - /** - * EmployeeUpworkJobsSearchCriterion - */ - @MultiORMOneToMany(() => EmployeeUpworkJobsSearchCriterion, (it) => it.occupation, { - /** Database cascade action on delete. */ - onDelete: 'CASCADE' - }) - employeeCriterions?: IEmployeeUpworkJobsSearchCriterion[]; - - /** - * JobPresetUpworkJobSearchCriterion - */ - @MultiORMOneToMany(() => JobPresetUpworkJobSearchCriterion, (it) => it.occupation, { - /** Database cascade action on delete. */ - onDelete: 'CASCADE' - }) - jobPresetCriterions?: IJobPresetUpworkJobSearchCriterion[]; } diff --git a/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.seed.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.seed.ts new file mode 100644 index 00000000000..e74ce88e1e5 --- /dev/null +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.seed.ts @@ -0,0 +1,35 @@ +import { DataSource } from 'typeorm'; +import { IOrganization, ITenant, JobPostSourceEnum } from '@gauzy/contracts'; +import { JobSearchOccupation } from './job-search-occupation.entity'; + +/** + * Creates default job search occupations. + * + * @param connection The connection to the data source for database operations. + * @param tenant The tenant for which occupations are created. + * @param organization The organization for which occupations are created. + * @returns A Promise that resolves with the created job search occupations. + */ +export const createDefaultJobSearchOccupations = async ( + connection: DataSource, + tenant: ITenant, + organization: IOrganization +): Promise => { + const upworkOccupations = [ + { name: 'DevOps Engineering', jobSourceOccupationId: '1110580753140797440' }, + { name: 'Project Management', jobSourceOccupationId: '1017484851352698979' } + ]; + + const occupations: JobSearchOccupation[] = upworkOccupations.map(occupation => { + const occ = new JobSearchOccupation(); + occ.jobSource = JobPostSourceEnum.UPWORK; + occ.organizationId = organization.id; + occ.tenantId = tenant.id; + occ.name = occupation.name; + occ.jobSourceOccupationId = occupation.jobSourceOccupationId; + return occ; + }); + + await connection.manager.save(occupations); + return occupations; +}; diff --git a/packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.service.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.service.ts similarity index 56% rename from packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.service.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.service.ts index 3af9bfce226..632a0246c8c 100644 --- a/packages/core/src/employee-job-preset/job-search-occupation/job-search-occupation.service.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/job-search-occupation.service.ts @@ -1,16 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { TenantAwareCrudService } from './../../core/crud'; +import { TenantAwareCrudService } from '@gauzy/core'; import { JobSearchOccupation } from './job-search-occupation.entity'; -import { MikroOrmJobSearchOccupationRepository } from './repository/mikro-orm-job-search-occupation.repository'; -import { TypeOrmJobSearchOccupationRepository } from './repository/type-orm-job-search-occupation.repository'; +import { MikroOrmJobSearchOccupationRepository, TypeOrmJobSearchOccupationRepository } from './repository'; @Injectable() export class JobSearchOccupationService extends TenantAwareCrudService { constructor( - @InjectRepository(JobSearchOccupation) typeOrmJobSearchOccupationRepository: TypeOrmJobSearchOccupationRepository, - mikroOrmJobSearchOccupationRepository: MikroOrmJobSearchOccupationRepository ) { super(typeOrmJobSearchOccupationRepository, mikroOrmJobSearchOccupationRepository); diff --git a/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/repository/index.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/repository/index.ts new file mode 100644 index 00000000000..ab37d2d6548 --- /dev/null +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/repository/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-job-search-occupation.repository'; +export * from './type-orm-job-search-occupation.repository'; diff --git a/packages/core/src/employee-job-preset/job-search-occupation/repository/mikro-orm-job-search-occupation.repository.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/repository/mikro-orm-job-search-occupation.repository.ts similarity index 63% rename from packages/core/src/employee-job-preset/job-search-occupation/repository/mikro-orm-job-search-occupation.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-occupation/repository/mikro-orm-job-search-occupation.repository.ts index 5b247cc22dd..b0f730a2d60 100644 --- a/packages/core/src/employee-job-preset/job-search-occupation/repository/mikro-orm-job-search-occupation.repository.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/repository/mikro-orm-job-search-occupation.repository.ts @@ -1,4 +1,4 @@ -import { MikroOrmBaseEntityRepository } from '../../../core/repository/mikro-orm-base-entity.repository'; +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; import { JobSearchOccupation } from '../job-search-occupation.entity'; export class MikroOrmJobSearchOccupationRepository extends MikroOrmBaseEntityRepository { } diff --git a/packages/core/src/employee-job-preset/job-search-occupation/repository/type-orm-job-search-occupation.repository.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-occupation/repository/type-orm-job-search-occupation.repository.ts similarity index 100% rename from packages/core/src/employee-job-preset/job-search-occupation/repository/type-orm-job-search-occupation.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-occupation/repository/type-orm-job-search-occupation.repository.ts diff --git a/packages/core/src/employee-job-preset/job-search-preset.controller.ts b/packages/plugins/job-search/src/employee-job-preset/job-search-preset.controller.ts similarity index 51% rename from packages/core/src/employee-job-preset/job-search-preset.controller.ts rename to packages/plugins/job-search/src/employee-job-preset/job-search-preset.controller.ts index e551caae3d5..85918b4349c 100644 --- a/packages/core/src/employee-job-preset/job-search-preset.controller.ts +++ b/packages/plugins/job-search/src/employee-job-preset/job-search-preset.controller.ts @@ -2,12 +2,10 @@ import { Controller, HttpStatus, Get, Query, Post, Body, Param, Delete } from '@ import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { IGetJobPresetCriterionInput, IJobPreset, IMatchingCriterions } from '@gauzy/contracts'; import { GauzyAIService } from '@gauzy/integration-ai'; +import { EmployeeService, UUIDValidationPipe, UseValidationPipe } from '@gauzy/core'; import { JobPresetService } from './job-preset.service'; import { JobPreset } from './job-preset.entity'; import { JobPresetUpworkJobSearchCriterion } from './job-preset-upwork-job-search-criterion.entity'; -import { EmployeeUpworkJobsSearchCriterion } from './employee-upwork-jobs-search-criterion.entity'; -import { EmployeeService } from '../employee/employee.service'; -import { UUIDValidationPipe, UseValidationPipe } from './../shared/pipes'; import { JobPresetQueryDTO } from './dto'; @ApiTags('JobSearchPreset') @@ -17,12 +15,18 @@ export class JobSearchPresetController { private readonly jobPresetService: JobPresetService, private readonly employeeService: EmployeeService, private readonly gauzyAIService: GauzyAIService - ) {} + ) { } + /** + * Retrieves all employee job presets. + * + * @param input The query parameters for filtering job presets. + * @returns A Promise that resolves to the retrieved job presets. + */ @ApiOperation({ summary: 'Find all employee job posts' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found employee job preset', + description: 'Found employee job presets', type: JobPreset }) @ApiResponse({ @@ -34,16 +38,22 @@ export class JobSearchPresetController { async getAll(@Query() input: JobPresetQueryDTO) { console.log('GetAll Presets called. We will sync all employees now'); - // TODO: we can actually sync just for one employee if data.employeeId is defined - + // Synchronize all active employees const employees = await this.employeeService.findAllActive(); - await this.gauzyAIService.syncEmployees(employees); + // Retrieve all job presets based on the provided query parameters return await this.jobPresetService.getAll(input); } - @ApiOperation({ summary: 'Find all employee job posts' }) + /** + * Retrieves an employee job preset by its ID. + * + * @param presetId The ID of the job preset to retrieve. + * @param request The query parameters for filtering job presets. + * @returns A Promise that resolves to the retrieved job preset. + */ + @ApiOperation({ summary: 'Find an employee job preset by ID' }) @ApiResponse({ status: HttpStatus.OK, description: 'Found employee job preset', @@ -54,14 +64,23 @@ export class JobSearchPresetController { description: 'Record not found' }) @Get(':id') - async get(@Param('id', UUIDValidationPipe) presetId: string, @Query() request: IGetJobPresetCriterionInput) { - return this.jobPresetService.get(presetId, request); + async get( + @Param('id', UUIDValidationPipe) presetId: string, + @Query() request: IGetJobPresetCriterionInput + ) { + return await this.jobPresetService.get(presetId, request); } - @ApiOperation({ summary: 'Find all employee job posts' }) + /** + * Retrieves job preset criteria for a specific job preset by its ID. + * + * @param presetId The ID of the job preset for which to retrieve criteria. + * @returns A Promise that resolves to the job preset criteria. + */ + @ApiOperation({ summary: 'Find job preset criteria by job preset ID' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found employee job preset', + description: 'Found job preset criteria', type: JobPresetUpworkJobSearchCriterion }) @ApiResponse({ @@ -73,30 +92,43 @@ export class JobSearchPresetController { return this.jobPresetService.getJobPresetCriterion(presetId); } - @ApiOperation({ summary: 'Find all employee job posts' }) + /** + * Creates a new job preset. + * + * @param request The job preset data. + * @returns A Promise that resolves to the created job preset. + */ + @ApiOperation({ summary: 'Create a new job preset' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found employee job preset', - type: EmployeeUpworkJobsSearchCriterion + description: 'Job preset created successfully', + type: JobPreset }) @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Record not found' + status: HttpStatus.BAD_REQUEST, + description: 'Invalid job preset data' }) @Post() async createJobPreset(@Body() request: IJobPreset) { return this.jobPresetService.createJobPreset(request); } - @ApiOperation({ summary: 'Find all employee job posts' }) + /** + * Saves or updates job preset criteria for a specific job preset. + * + * @param jobPresetId The ID of the job preset. + * @param request The criteria data to save or update. + * @returns A Promise that resolves to the saved or updated job preset criteria. + */ + @ApiOperation({ summary: 'Save or update job preset criteria' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found employee job preset', + description: 'Job preset criteria saved or updated successfully', type: JobPreset }) @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Record not found' + status: HttpStatus.BAD_REQUEST, + description: 'Invalid job preset criteria data' }) @Post(':jobPresetId/criterion') async saveUpdate( @@ -109,18 +141,24 @@ export class JobSearchPresetController { }); } - @ApiOperation({ summary: 'Find all employee job posts' }) + /** + * Deletes a job preset criterion by its ID. + * + * @param criterionId The ID of the job preset criterion to delete. + * @returns A Promise that resolves to the deleted job preset criterion. + */ + @ApiOperation({ summary: 'Delete job preset criterion by ID' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found employee job preset', + description: 'Job preset criterion deleted successfully', type: JobPreset }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Record not found' + description: 'Job preset criterion not found' }) @Delete('criterion/:criterionId') - async deleteJobPresetCriterion(@Param('criterionId', UUIDValidationPipe) creationId: string) { - return this.jobPresetService.deleteJobPresetCriterion(creationId); + async deleteJobPresetCriterion(@Param('criterionId', UUIDValidationPipe) criterionId: string) { + return this.jobPresetService.deleteJobPresetCriterion(criterionId); } } diff --git a/packages/plugins/job-search/src/employee-job-preset/job-seeder.service.ts b/packages/plugins/job-search/src/employee-job-preset/job-seeder.service.ts new file mode 100644 index 00000000000..664e2f01cdd --- /dev/null +++ b/packages/plugins/job-search/src/employee-job-preset/job-seeder.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import * as chalk from 'chalk'; +import { environment } from '@gauzy/config'; +import { ConnectionEntityManager, SeedDataService } from '@gauzy/core'; +import { createDefaultJobSearchCategories } from './job-search-category/job-search-category.seed'; +import { createDefaultJobSearchOccupations } from './job-search-occupation/job-search-occupation.seed'; + +/** + * Service dealing with help center based operations. + * + * @class + */ +@Injectable() +export class JobSeederService { + /** + * Create an instance of class. + * + * @constructs + * + */ + constructor( + private readonly connectionEntityManager: ConnectionEntityManager, + private readonly seeder: SeedDataService + ) { } + + /** + * Seeds job data into the database. + * + * This function seeds default job search categories and occupations using the provided connection, + * tenant, and default organization. It logs the seeding process and any errors encountered. + */ + public async seedDefaultJobsData(): Promise { + try { + // Log the start of the seeding process + this.seeder.log(chalk.green(`🌱 SEEDING ${environment.production ? 'PRODUCTION' : ''} JOBS DATABASE...`)); + + // Seed default job search categories + await this.seeder.tryExecute( + 'Default Job Search Categories', + createDefaultJobSearchCategories( + this.connectionEntityManager.rawConnection, + this.seeder.tenant, + this.seeder.defaultOrganization + ) + ); + + // Seed default job search occupations + await this.seeder.tryExecute( + 'Default Job Search Occupations', + createDefaultJobSearchOccupations( + this.connectionEntityManager.rawConnection, + this.seeder.tenant, + this.seeder.defaultOrganization + ) + ); + + // Log the completion of the seeding process + this.seeder.log(chalk.green(`✅ SEEDED ${environment.production ? 'PRODUCTION' : ''} JOBS DATABASE`)); + } catch (error) { + // Log any errors encountered during the seeding process + console.log('Error while job data seeding: %s', error.message); + } + } +} diff --git a/packages/core/src/employee-job-preset/repository/mikro-orm-employee-upwork-jobs-search-criterion.entity.repository.ts b/packages/plugins/job-search/src/employee-job-preset/repository/mikro-orm-employee-upwork-jobs-search-criterion.entity.repository.ts similarity index 70% rename from packages/core/src/employee-job-preset/repository/mikro-orm-employee-upwork-jobs-search-criterion.entity.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/repository/mikro-orm-employee-upwork-jobs-search-criterion.entity.repository.ts index 4ebff6d2ad9..106cc2de9a1 100644 --- a/packages/core/src/employee-job-preset/repository/mikro-orm-employee-upwork-jobs-search-criterion.entity.repository.ts +++ b/packages/plugins/job-search/src/employee-job-preset/repository/mikro-orm-employee-upwork-jobs-search-criterion.entity.repository.ts @@ -1,4 +1,4 @@ -import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository'; +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; import { EmployeeUpworkJobsSearchCriterion } from '../employee-upwork-jobs-search-criterion.entity'; export class MikroOrmEmployeeUpworkJobsSearchCriterionRepository extends MikroOrmBaseEntityRepository { } diff --git a/packages/core/src/employee-job-preset/repository/mikro-orm-job-preset-upwork-job-search-criterion.repository.ts b/packages/plugins/job-search/src/employee-job-preset/repository/mikro-orm-job-preset-upwork-job-search-criterion.repository.ts similarity index 70% rename from packages/core/src/employee-job-preset/repository/mikro-orm-job-preset-upwork-job-search-criterion.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/repository/mikro-orm-job-preset-upwork-job-search-criterion.repository.ts index 29370a01afd..eeacff17e0a 100644 --- a/packages/core/src/employee-job-preset/repository/mikro-orm-job-preset-upwork-job-search-criterion.repository.ts +++ b/packages/plugins/job-search/src/employee-job-preset/repository/mikro-orm-job-preset-upwork-job-search-criterion.repository.ts @@ -1,4 +1,4 @@ -import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository'; +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; import { JobPresetUpworkJobSearchCriterion } from '../job-preset-upwork-job-search-criterion.entity'; export class MikroOrmJobPresetUpworkJobSearchCriterionRepository extends MikroOrmBaseEntityRepository { } diff --git a/packages/core/src/employee-job-preset/repository/mikro-orm-job-preset.repository.ts b/packages/plugins/job-search/src/employee-job-preset/repository/mikro-orm-job-preset.repository.ts similarity index 58% rename from packages/core/src/employee-job-preset/repository/mikro-orm-job-preset.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/repository/mikro-orm-job-preset.repository.ts index 8c273a43c15..101a7c2cf9e 100644 --- a/packages/core/src/employee-job-preset/repository/mikro-orm-job-preset.repository.ts +++ b/packages/plugins/job-search/src/employee-job-preset/repository/mikro-orm-job-preset.repository.ts @@ -1,4 +1,4 @@ -import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository'; +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; import { JobPreset } from '../job-preset.entity'; export class MikroOrmJobPresetRepository extends MikroOrmBaseEntityRepository { } diff --git a/packages/core/src/employee-job-preset/repository/type-orm-job-preset-upwork-job-search-criterion.repository.ts b/packages/plugins/job-search/src/employee-job-preset/repository/type-orm-job-preset-upwork-job-search-criterion.repository.ts similarity index 100% rename from packages/core/src/employee-job-preset/repository/type-orm-job-preset-upwork-job-search-criterion.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/repository/type-orm-job-preset-upwork-job-search-criterion.repository.ts diff --git a/packages/core/src/employee-job-preset/repository/type-orm-job-preset.repository.ts b/packages/plugins/job-search/src/employee-job-preset/repository/type-orm-job-preset.repository.ts similarity index 100% rename from packages/core/src/employee-job-preset/repository/type-orm-job-preset.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/repository/type-orm-job-preset.repository.ts diff --git a/packages/core/src/employee-job-preset/repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository.ts b/packages/plugins/job-search/src/employee-job-preset/repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository.ts similarity index 100% rename from packages/core/src/employee-job-preset/repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository.ts rename to packages/plugins/job-search/src/employee-job-preset/repository/typeorm-orm-employee-upwork-jobs-search-criterion.entity.repository.ts diff --git a/packages/core/src/employee-job/employee-job.controller.ts b/packages/plugins/job-search/src/employee-job/employee-job.controller.ts similarity index 83% rename from packages/core/src/employee-job/employee-job.controller.ts rename to packages/plugins/job-search/src/employee-job/employee-job.controller.ts index 603d016b5bd..b2d7bd286c1 100644 --- a/packages/core/src/employee-job/employee-job.controller.ts +++ b/packages/plugins/job-search/src/employee-job/employee-job.controller.ts @@ -9,14 +9,17 @@ import { IUpdateEmployeeJobPostAppliedResult, IVisibilityJobPostInput } from '@gauzy/contracts'; -import { UUIDValidationPipe, UseValidationPipe } from './../shared/pipes'; +import { UUIDValidationPipe, UseValidationPipe } from '@gauzy/core'; import { EmployeeJobPostService } from './employee-job.service'; import { EmployeeJobPost } from './employee-job.entity'; @ApiTags('EmployeeJobPost') -@Controller() +@Controller('/employee-job') export class EmployeeJobPostController { - constructor(private readonly employeeJobPostService: EmployeeJobPostService) { } + + constructor( + private readonly _employeeJobPostService: EmployeeJobPostService + ) { } /** * Find all employee job posts. @@ -35,8 +38,10 @@ export class EmployeeJobPostController { description: 'Record not found' }) @Get() - async findAll(@Query() input: IGetEmployeeJobPostInput): Promise> { - return this.employeeJobPostService.findAll(input); + async findAll( + @Query() input: IGetEmployeeJobPostInput + ): Promise> { + return await this._employeeJobPostService.findAll(input); } /** @@ -57,10 +62,12 @@ export class EmployeeJobPostController { }) @UseValidationPipe() // Assuming ValidationPipe is configured appropriately @Post('apply') - async apply(@Body() input: IEmployeeJobApplication): Promise { + async apply( + @Body() input: IEmployeeJobApplication + ): Promise { try { // Apply for the job using the service - const appliedJobPost = await this.employeeJobPostService.apply(input); + const appliedJobPost = await this._employeeJobPostService.apply(input); // If needed, perform additional logic here return appliedJobPost; @@ -91,10 +98,12 @@ export class EmployeeJobPostController { }) @UseValidationPipe() // Assuming ValidationPipe is configured appropriately @Post('updateApplied') - async updateApplied(@Body() input: IEmployeeJobApplication): Promise { + async updateApplied( + @Body() input: IEmployeeJobApplication + ): Promise { try { // Update the job application status using the service - const updatedJobPost = await this.employeeJobPostService.updateApplied(input); + const updatedJobPost = await this._employeeJobPostService.updateApplied(input); // If needed, perform additional logic here @@ -126,10 +135,12 @@ export class EmployeeJobPostController { }) @UseValidationPipe() // Assuming ValidationPipe is configured appropriately @Post('hide') - async updateVisibility(@Body() data: IVisibilityJobPostInput): Promise { + async updateVisibility( + @Body() data: IVisibilityJobPostInput + ): Promise { try { // Update the job visibility status using the service - const updatedJobPost = await this.employeeJobPostService.updateVisibility(data); + const updatedJobPost = await this._employeeJobPostService.updateVisibility(data); // If needed, perform additional logic here @@ -158,7 +169,7 @@ export class EmployeeJobPostController { // Validate the input structure if needed // Create a preliminary employee job application record using the service - const createdJobApplication = await this.employeeJobPostService.preProcessEmployeeJobApplication(input); + const createdJobApplication = await this._employeeJobPostService.preProcessEmployeeJobApplication(input); // If needed, perform additional logic here @@ -187,7 +198,7 @@ export class EmployeeJobPostController { ): Promise { try { // Retrieve AI-generated proposal for the employee job application using the service - const proposal = await this.employeeJobPostService.getEmployeeJobApplication(employeeJobApplicationId); + const proposal = await this._employeeJobPostService.getEmployeeJobApplication(employeeJobApplicationId); // If needed, perform additional logic here @@ -216,7 +227,7 @@ export class EmployeeJobPostController { ): Promise { try { // Generate AI proposal for the employee job application using the service - const aiProposal = await this.employeeJobPostService.generateAIProposal(employeeJobApplicationId); + const aiProposal = await this._employeeJobPostService.generateAIProposal(employeeJobApplicationId); // If needed, perform additional logic here diff --git a/packages/core/src/employee-job/employee-job.entity.ts b/packages/plugins/job-search/src/employee-job/employee-job.entity.ts similarity index 88% rename from packages/core/src/employee-job/employee-job.entity.ts rename to packages/plugins/job-search/src/employee-job/employee-job.entity.ts index 4444aae49d9..4dd54ccb7bd 100644 --- a/packages/core/src/employee-job/employee-job.entity.ts +++ b/packages/plugins/job-search/src/employee-job/employee-job.entity.ts @@ -1,10 +1,12 @@ + +import { Employee, Model } from '@gauzy/core'; import { IEmployeeJobPost, JobPostSourceEnum, JobPostStatusEnum, JobPostTypeEnum } from '@gauzy/contracts'; -import { Employee, JobPost, Model } from '../core/entities/internal'; +import { JobPost } from './jobPost.entity'; export class EmployeeJobPost extends Model implements IEmployeeJobPost { employeeId: string; diff --git a/packages/plugins/job-search/src/employee-job/employee-job.module.ts b/packages/plugins/job-search/src/employee-job/employee-job.module.ts new file mode 100644 index 00000000000..662742fb73a --- /dev/null +++ b/packages/plugins/job-search/src/employee-job/employee-job.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { EmployeeModule, IntegrationTenantModule } from '@gauzy/core'; +import { GauzyAIModule } from '@gauzy/integration-ai'; +import { EmployeeJobPostService } from './employee-job.service'; +import { EmployeeJobPostController } from './employee-job.controller'; + +@Module({ + imports: [EmployeeModule, IntegrationTenantModule, GauzyAIModule.forRoot()], + controllers: [EmployeeJobPostController], + providers: [EmployeeJobPostService], + exports: [] +}) +export class EmployeeJobPostModule { } diff --git a/packages/core/src/employee-job/employee-job.service.ts b/packages/plugins/job-search/src/employee-job/employee-job.service.ts similarity index 75% rename from packages/core/src/employee-job/employee-job.service.ts rename to packages/plugins/job-search/src/employee-job/employee-job.service.ts index 5601c932f6a..69a6c336b96 100644 --- a/packages/core/src/employee-job/employee-job.service.ts +++ b/packages/plugins/job-search/src/employee-job/employee-job.service.ts @@ -5,7 +5,6 @@ import { environment as env } from '@gauzy/config'; import { GauzyAIService } from '@gauzy/integration-ai'; import { IEmployeeJobApplication, - ICountry, IEmployee, IEmployeeJobPost, IGetEmployeeJobPostInput, @@ -20,19 +19,15 @@ import { IntegrationEnum, IntegrationEntity } from '@gauzy/contracts'; -import { RequestContext } from './../core/context'; -import { IntegrationTenantService } from './../integration-tenant/integration-tenant.service'; -import { EmployeeService } from '../employee/employee.service'; -import { CountryService } from './../country/country.service'; +import { EmployeeService, IntegrationTenantService, RequestContext } from '@gauzy/core'; import { EmployeeJobPost } from './employee-job.entity'; import { JobPost } from './jobPost.entity'; @Injectable() export class EmployeeJobPostService { constructor( - private readonly employeeService: EmployeeService, - private readonly gauzyAIService: GauzyAIService, - private readonly countryService: CountryService, + private readonly _employeeService: EmployeeService, + private readonly _gauzyAIService: GauzyAIService, private readonly _integrationTenantService: IntegrationTenantService, ) { } @@ -45,7 +40,7 @@ export class EmployeeJobPostService { * @param providerJobId Unique job id in the provider, e.g. in Upwork */ public async updateVisibility(input: IVisibilityJobPostInput): Promise { - return await this.gauzyAIService.updateVisibility(input); + return await this._gauzyAIService.updateVisibility(input); } /** @@ -56,13 +51,14 @@ export class EmployeeJobPostService { * @param providerJobId Unique job id in the provider, e.g. Job Id in Upwork */ public async updateApplied(input: IEmployeeJobApplication): Promise { - return await this.gauzyAIService.updateApplied(input); + return await this._gauzyAIService.updateApplied(input); } /** - * Apply for a job - * @param input - * @returns + * Applies for a job by converting HTML content to plain text and then forwarding the application to a service. + * + * @param input The input data for applying to the job, including the job application proposal. + * @returns A promise that resolves to the result of the job application. */ public async apply(input: IEmployeeJobApplication): Promise { try { @@ -72,8 +68,10 @@ export class EmployeeJobPostService { input.proposal = plainText; } catch (error) { console.log('Error while applying job', error); + // Handle the error here, you might want to throw it or return a specific error result } - return await this.gauzyAIService.apply(input); + // Return the result of applying for the job + return await this._gauzyAIService.apply(input); } /** @@ -81,7 +79,7 @@ export class EmployeeJobPostService { * @param data */ public async findAll(data: IGetEmployeeJobPostInput): Promise> { - const employees = await this.employeeService.findAllActive(); + const employees = await this._employeeService.findAllActive(); let jobs: IPagination; try { @@ -109,7 +107,7 @@ export class EmployeeJobPostService { entityType: IntegrationEntity.JOB_MATCHING }); - const result = await this.gauzyAIService.getEmployeesJobPosts(data); + const result = await this._gauzyAIService.getEmployeesJobPosts(data); if (result === null) { if (env.production) { // OK, so for some reason connection go Gauzy AI failed, we can't get jobs ... @@ -119,16 +117,15 @@ export class EmployeeJobPostService { }; } else { // In development, even if connection failed, we want to show fake jobs in UI - jobs = await this.getRandomEmployeeJobPosts(employees, data.page, data.limit); + jobs = await this.getRandomEmployeeJobPosts(employees, data.limit); } } else { - const jobsConverted = result.items.map((jo) => { - if (jo.employeeId) { - const employee = employees.find((emp) => emp.id === jo.employeeId); - jo.employee = employee; + const jobsConverted = result.items.map((job) => { + if (job.employeeId) { + const employee = employees.find((employee) => employee.id === job.employeeId); + job.employee = employee; } - - return jo; + return job; }); jobs = { @@ -138,12 +135,12 @@ export class EmployeeJobPostService { } } else { // If integration not enabled, we want to show fake jobs in UI - jobs = await this.getRandomEmployeeJobPosts(employees, data.page, data.limit); + jobs = await this.getRandomEmployeeJobPosts(employees, data.limit); } } else { // If it's production, we should return empty here because we don't want fake jobs in production if (env.production === false) { - jobs = await this.getRandomEmployeeJobPosts(employees, data.page, data.limit); + jobs = await this.getRandomEmployeeJobPosts(employees, data.limit); } else { jobs = { items: [], @@ -168,7 +165,7 @@ export class EmployeeJobPostService { * @returns */ public async preProcessEmployeeJobApplication(params: IEmployeeJobApplication) { - return await this.gauzyAIService.preProcessEmployeeJobApplication(params); + return await this._gauzyAIService.preProcessEmployeeJobApplication(params); } /** @@ -177,7 +174,7 @@ export class EmployeeJobPostService { * @param employeeJobApplicationId */ public async generateAIProposal(employeeJobApplicationId: string): Promise { - return await this.gauzyAIService.generateAIProposalForEmployeeJobApplication(employeeJobApplicationId); + return await this._gauzyAIService.generateAIProposalForEmployeeJobApplication(employeeJobApplicationId); } /** @@ -187,26 +184,27 @@ export class EmployeeJobPostService { * @returns */ public async getEmployeeJobApplication(employeeJobApplicationId: string) { - return await this.gauzyAIService.getEmployeeJobApplication(employeeJobApplicationId); + return await this._gauzyAIService.getEmployeeJobApplication(employeeJobApplicationId); } - private async getRandomEmployeeJobPosts( - employees?: IEmployee[], - page = 0, - limit = 10 - ): Promise> { - const { items: countries = [] as ICountry[] } = await this.countryService.findAll(); - + /** + * Generates random employee job posts. + * + * @param employees The array of employees to assign to job posts. + * @param limit The maximum number of job posts to generate. + * @returns A promise that resolves to an object containing paginated employee job posts. + */ + private async getRandomEmployeeJobPosts(employees: IEmployee[] = [], limit = 10): Promise> { const employeesJobs: EmployeeJobPost[] = []; + for (let i = 0; i < limit; i++) { const employee = faker.helpers.arrayElement(employees); const jobPostEmployee = new EmployeeJobPost({ employeeId: employee ? employee.id : null, employee: employee }); - const job = new JobPost({ - country: faker.helpers.arrayElement(countries).isoCode, + country: faker.location.countryCode(), category: faker.person.jobTitle(), title: faker.lorem.sentence(), description: faker.lorem.sentences(3), @@ -215,7 +213,6 @@ export class EmployeeJobPostService { jobSource: faker.helpers.arrayElement(Object.values(JobPostSourceEnum)), jobType: faker.helpers.arrayElement(Object.values(JobPostTypeEnum)) }); - jobPostEmployee.jobPost = job; employeesJobs.push(jobPostEmployee); } diff --git a/packages/plugins/job-search/src/employee-job/index.ts b/packages/plugins/job-search/src/employee-job/index.ts new file mode 100644 index 00000000000..c2d74d95531 --- /dev/null +++ b/packages/plugins/job-search/src/employee-job/index.ts @@ -0,0 +1,2 @@ +export * from './employee-job.entity'; +export * from './jobPost.entity'; diff --git a/packages/core/src/employee-job/jobPost.entity.ts b/packages/plugins/job-search/src/employee-job/jobPost.entity.ts similarity index 94% rename from packages/core/src/employee-job/jobPost.entity.ts rename to packages/plugins/job-search/src/employee-job/jobPost.entity.ts index 8ecff356250..cc552b982db 100644 --- a/packages/core/src/employee-job/jobPost.entity.ts +++ b/packages/plugins/job-search/src/employee-job/jobPost.entity.ts @@ -1,10 +1,10 @@ +import { Model } from '@gauzy/core'; import { IJobPost, JobPostSourceEnum, JobPostStatusEnum, JobPostTypeEnum } from '@gauzy/contracts'; -import { Model } from '../core/entities/internal'; export class JobPost extends Model implements IJobPost { jobPostId: string; diff --git a/packages/plugins/job-search/src/index.ts b/packages/plugins/job-search/src/index.ts new file mode 100644 index 00000000000..16da6293295 --- /dev/null +++ b/packages/plugins/job-search/src/index.ts @@ -0,0 +1 @@ +export * from './job-search.plugin'; diff --git a/packages/plugins/job-search/src/job-search.plugin.ts b/packages/plugins/job-search/src/job-search.plugin.ts new file mode 100644 index 00000000000..d6b6ac5ad81 --- /dev/null +++ b/packages/plugins/job-search/src/job-search.plugin.ts @@ -0,0 +1,81 @@ + +import * as chalk from 'chalk'; +import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy, IOnPluginSeedable } from '@gauzy/plugin'; +import { SeederModule } from '@gauzy/core'; +import { ApplicationPluginConfig } from '@gauzy/common'; +import { EmployeeJobPostModule } from './employee-job/employee-job.module'; +import { EmployeeJobPresetModule, entities } from './employee-job-preset/employee-job-preset.module'; +import { JobPreset } from './employee-job-preset/job-preset.entity'; +import { JobSeederService } from './employee-job-preset/job-seeder.service'; + +@Plugin({ + imports: [EmployeeJobPostModule, EmployeeJobPresetModule, SeederModule], + entities: [...entities], + configuration: (config: ApplicationPluginConfig) => { + // Configuration object for custom fields in the Employee entity. + config.customFields.Employee.push({ + propertyPath: 'jobPresets', + type: 'relation', + relationType: 'many-to-many', + entity: JobPreset, + inverseSide: (it: JobPreset) => it.employees + }); + return config; + }, + providers: [JobSeederService] +}) +export class JobSearchPlugin implements IOnPluginBootstrap, IOnPluginDestroy, IOnPluginSeedable { + + // We disable by default additional logging for each event to avoid cluttering the logs + private logEnabled = true; + + constructor(private readonly jobSeederService: JobSeederService) { } + + /** + * Called when the plugin is being initialized. + */ + onPluginBootstrap(): void | Promise { + if (this.logEnabled) { + console.log(`${JobSearchPlugin.name} is being bootstrapped...`); + } + } + + /** + * Called when the plugin is being destroyed. + */ + onPluginDestroy(): void | Promise { + if (this.logEnabled) { + console.log(`${JobSearchPlugin.name} is being destroyed...`); + } + } + + /** + * Seed default data for the plugin. + */ + async onPluginDefaultSeed() { + try { + await this.jobSeederService.seedDefaultJobsData(); + + if (this.logEnabled) { + console.log(chalk.green(`Default data seeded successfully for ${JobSearchPlugin.name}.`)); + } + } catch (error) { + console.error(chalk.red(`Error seeding default data for ${JobSearchPlugin.name}:`, error)); + } + } + + /** + * Seed random data for the plugin. + */ + async onPluginRandomSeed() { + try { + // Add your random data seeding logic here + + if (this.logEnabled) { + console.log(chalk.green(`Random data seeded successfully for ${JobSearchPlugin.name}.`)); + } + } catch (error) { + console.error(chalk.red(`Error seeding random data for ${JobSearchPlugin.name}:`, error)); + } + } +} diff --git a/packages/plugins/job-search/tsconfig.build.json b/packages/plugins/job-search/tsconfig.build.json new file mode 100644 index 00000000000..ea6be8e9a50 --- /dev/null +++ b/packages/plugins/job-search/tsconfig.build.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.json" +} diff --git a/packages/plugins/job-search/tsconfig.json b/packages/plugins/job-search/tsconfig.json new file mode 100644 index 00000000000..999d1cb59b2 --- /dev/null +++ b/packages/plugins/job-search/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "sourceMap": true, + "baseUrl": "./src", + "rootDir": "./src", + "outDir": "./dist", + "types": ["node", "jest"] + }, + "include": ["./src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/plugins/job-search/tsconfig.spec.json b/packages/plugins/job-search/tsconfig.spec.json new file mode 100644 index 00000000000..9f405553401 --- /dev/null +++ b/packages/plugins/job-search/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/packages/plugins/job-search/tslint.json b/packages/plugins/job-search/tslint.json new file mode 100644 index 00000000000..74516cfb290 --- /dev/null +++ b/packages/plugins/job-search/tslint.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tslint.json", + "rules": [] +} diff --git a/packages/plugins/knowledge-base/src/knowledge-base.plugin.ts b/packages/plugins/knowledge-base/src/knowledge-base.plugin.ts index d2387c75b18..024f5455abe 100644 --- a/packages/plugins/knowledge-base/src/knowledge-base.plugin.ts +++ b/packages/plugins/knowledge-base/src/knowledge-base.plugin.ts @@ -5,8 +5,7 @@ import { GauzyCorePlugin, IOnPluginBootstrap, IOnPluginDestroy, - IOnPluginWithDefaultSeed, - IOnPluginWithRandomSeed + IOnPluginSeedable } from '@gauzy/plugin'; import { HelpCenterAuthor, HelpCenterAuthorModule } from './help-center-author'; import { HelpCenter, HelpCenterModule } from './help-center'; @@ -17,20 +16,11 @@ import { import { HelpCenterSeederService } from './help-center-seeder.service'; @GauzyCorePlugin({ - imports: [ - HelpCenterModule, - HelpCenterArticleModule, - HelpCenterAuthorModule, - SeederModule - ], - entities: [ - HelpCenter, - HelpCenterArticle, - HelpCenterAuthor - ], + imports: [HelpCenterModule, HelpCenterArticleModule, HelpCenterAuthorModule, SeederModule], + entities: [HelpCenter, HelpCenterArticle, HelpCenterAuthor], providers: [HelpCenterSeederService] }) -export class KnowledgeBasePlugin implements IOnPluginBootstrap, IOnPluginDestroy, IOnPluginWithDefaultSeed, IOnPluginWithRandomSeed { +export class KnowledgeBasePlugin implements IOnPluginBootstrap, IOnPluginDestroy, IOnPluginSeedable { // We disable by default additional logging for each event to avoid cluttering the logs private logEnabled = true; diff --git a/yarn.lock b/yarn.lock index 5b80bd09613..c0da60aed4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6767,76 +6767,76 @@ resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== -"@mikro-orm/better-sqlite@^6.1.12": - version "6.1.12" - resolved "https://registry.yarnpkg.com/@mikro-orm/better-sqlite/-/better-sqlite-6.1.12.tgz#6a38251143f9b4a1d751a333a48bce7c0b628c43" - integrity sha512-7uupNjdoi8FgCfZ8sacC1Pau0xkOO+oRcOtQpa1Ddyh/8em/CMuPbjCzY04HrfAU0JPCL9KGDdL+4hleB3GH5Q== +"@mikro-orm/better-sqlite@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@mikro-orm/better-sqlite/-/better-sqlite-6.2.1.tgz#c7264f57889362d87007534bacc64a2def5562fc" + integrity sha512-sUNSac9UlOSwdaEOeyHQLgnjU8Q/M9+e/qwEBGVEgkocBoH/lJwYHxWnDDgzMFpliPJb3FpgvpRSxgmm7qFGGA== dependencies: - "@mikro-orm/knex" "6.1.12" - better-sqlite3 "8.7.0" + "@mikro-orm/knex" "6.2.1" + better-sqlite3 "9.5.0" fs-extra "11.2.0" sqlstring-sqlite "0.1.1" -"@mikro-orm/core@^6.1.12": - version "6.1.12" - resolved "https://registry.yarnpkg.com/@mikro-orm/core/-/core-6.1.12.tgz#753f5bf0461efaad8ced9d15220bed5937dfdf2f" - integrity sha512-51/1iBdXoF+bODJMpW8cUsr1TsieIJobiAX4g9A6CgBU6v95vwzyEQRo9v73i+YuPfrjH4YrrSbRaAr1tKe38A== +"@mikro-orm/core@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@mikro-orm/core/-/core-6.2.1.tgz#5ad75bb9ec7a955355396cbb44633d93aa092a93" + integrity sha512-qx7Tqm95Mmni/9Rfe7rNttHPkTLPltDQzAFGsd6V3R1HVlnwDsBKkNT0mChZ6nSgJvtwDLH0C0YUttn+IRrhPA== dependencies: dataloader "2.2.2" dotenv "16.4.5" esprima "4.0.1" fs-extra "11.2.0" globby "11.1.0" - mikro-orm "6.1.12" - reflect-metadata "0.2.1" + mikro-orm "6.2.1" + reflect-metadata "0.2.2" -"@mikro-orm/knex@6.1.12", "@mikro-orm/knex@^6.1.12": - version "6.1.12" - resolved "https://registry.yarnpkg.com/@mikro-orm/knex/-/knex-6.1.12.tgz#a1e3cbe44c1d726cd0139c9b08c645cc7bffc885" - integrity sha512-bGRDTM13ASYcmte8BglikDwfoYmCo8YUW5LY4Mn5GUCyzjLV7XP23SrTaerLIxXlNiTnJhTO+On3cOyndFwHpw== +"@mikro-orm/knex@6.2.1", "@mikro-orm/knex@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@mikro-orm/knex/-/knex-6.2.1.tgz#9e785aab3dd9b6370d7ac8dd3408a295f42ac3a5" + integrity sha512-PZwCof0bCRcc8n90IyLgvpz4Pk+fva4RFo1kxaU+48M+QBDYtXpWU36mz32lcPP4fpt0fgLjEz0iLaSqaNy5pA== dependencies: fs-extra "11.2.0" knex "3.1.0" sqlstring "2.3.3" -"@mikro-orm/mongodb@^6.1.12": - version "6.1.12" - resolved "https://registry.yarnpkg.com/@mikro-orm/mongodb/-/mongodb-6.1.12.tgz#802cb487f7635407b46e982018c94cb422506c08" - integrity sha512-wUqiHTQkGQjCjK0OV/E1epLs5QyzgHCxC0RyQLbu4OAwrEHHenwyNibD3Jj0GSAJ38AKK7CrynDMYWtFjVHbgg== +"@mikro-orm/mongodb@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@mikro-orm/mongodb/-/mongodb-6.2.1.tgz#62354bc5ba757341325e0be2ed942139e9017799" + integrity sha512-9rVDPmUh2jNlZKs2Pp7V+YOA4FISioEi+EvWFYVExbVuMWO67+iO/E6iWZZtWL/dkR4kwTKrfSOI6eckZmN6xw== dependencies: bson "^6.4.0" mongodb "6.5.0" -"@mikro-orm/mysql@^6.1.12": - version "6.1.12" - resolved "https://registry.yarnpkg.com/@mikro-orm/mysql/-/mysql-6.1.12.tgz#b1f2743e49ae0e0ea98f7d39b90711b16323b7fc" - integrity sha512-MgK7LT9jzDjVCNrqwuTaf13vjMmlORgjgTbfxl+tw+Gjab8dXIhNeztdxKYXUnGvWR8k0e43VgVgXf+ZhbookA== +"@mikro-orm/mysql@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@mikro-orm/mysql/-/mysql-6.2.1.tgz#21db5eefcc1499a0c26800d5c193325e08e6eced" + integrity sha512-WIevYU7C32lhvcctkfus7fol8kTKeWUS6OaIqW9nRUvMfXq8zJkweKf0hJt2jt3XmanFcHJ+O9rTIEqXQ0lTIw== dependencies: - "@mikro-orm/knex" "6.1.12" - mysql2 "3.9.2" + "@mikro-orm/knex" "6.2.1" + mysql2 "3.9.4" "@mikro-orm/nestjs@^5.2.3": version "5.2.3" resolved "https://registry.yarnpkg.com/@mikro-orm/nestjs/-/nestjs-5.2.3.tgz#204ef21781fce6b6e2423019bad515186228064d" integrity sha512-JMxaXrNXlo6j59D3LWMC1tEC1a5JanCtqdfv91JUH0sfVZh97SsjQ9K794BY3JWIUKSFyQwpnLxYZ0Ash/BlPA== -"@mikro-orm/postgresql@^6.1.12": - version "6.1.12" - resolved "https://registry.yarnpkg.com/@mikro-orm/postgresql/-/postgresql-6.1.12.tgz#65fc41a9096ced969e746dc1d4d13c3142c90d71" - integrity sha512-wiegkUtiQ3hnz+Dk0QaXR+A32U3r3yWF/VM+pBIQ/4aR1EBCaOJaGkQD+oncuznpeRVbT7Z/dYt2Fkm8PdM8Jw== +"@mikro-orm/postgresql@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@mikro-orm/postgresql/-/postgresql-6.2.1.tgz#970eda131f9ffd1b645bd7732181794b4b1003a0" + integrity sha512-W/Gen9B8kUa7FdPg1jO1b6Ks1usegg6YEKg3RUgLdNB7e6CKm8VfkZdZoxaWeoIF2TZn9px6vF3tZMwJ9NOvVA== dependencies: - "@mikro-orm/knex" "6.1.12" - pg "8.11.3" + "@mikro-orm/knex" "6.2.1" + pg "8.11.5" postgres-array "3.0.2" postgres-date "2.1.0" postgres-interval "4.0.2" -"@mikro-orm/sqlite@^6.1.12": - version "6.1.12" - resolved "https://registry.yarnpkg.com/@mikro-orm/sqlite/-/sqlite-6.1.12.tgz#342946dc7ed58f7d4ec64c973f845a1fda5cde2a" - integrity sha512-IOv0s5UYdO2dzCiHGpTcceo2FOZtddnMmlNmRkBXhKzYkYUEut0qJJqt20d8oJsMJuEk8Et7/MAgdJT5FbWTzA== +"@mikro-orm/sqlite@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@mikro-orm/sqlite/-/sqlite-6.2.1.tgz#233f1f771157fac8ba7a3abe9491d34f8b3f5048" + integrity sha512-EefUyXYlNzzUwMuvYFR9QNU+Z1Sl9BkmEyaLU2pZYxT5Os9g29HlnFKhtYua9FgVCMPkIUWZx23eS8a3gWB42w== dependencies: - "@mikro-orm/knex" "6.1.12" + "@mikro-orm/knex" "6.2.1" fs-extra "11.2.0" sqlite3 "5.1.7" sqlstring-sqlite "0.1.1" @@ -7738,12 +7738,6 @@ version "15.9.7" resolved "https://registry.yarnpkg.com/@nrwl/linter/-/linter-15.9.7.tgz#8cb2b02e945fdd461cdd8f52c470ab55c5ac0ab3" integrity sha512-PSbdBodqpbw1jmCWGLB1kxKRM8wpbonhZ3V133aLwb8P9c4q0aN7b3Z9VvtNKSS9eaLxYLu50BGiOahMIf4VXg== - dependencies: - "@nrwl/devkit" "15.9.7" - "@nrwl/js" "15.9.7" - "@phenomnomnominal/tsquery" "4.1.1" - tmp "~0.2.1" - tslib "^2.3.0" "@nrwl/nest@15.9.4": version "15.9.4" @@ -7878,8 +7872,6 @@ version "15.9.7" resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-15.9.7.tgz#c0e78c99caa6742762f7558f20d8524bc9015e97" integrity sha512-OBnHNvQf3vBH0qh9YnvBQQWyyFZ+PWguF6dJ8+1vyQYlrLVk/XZ8nJ4ukWFb+QfPv/O8VBmqaofaOI9aFC4yTw== - dependencies: - nx "15.9.7" "@nrwl/web@^15.0.0": version "15.9.7" @@ -11525,14 +11517,7 @@ dependencies: image-size "*" -"@types/ioredis4@npm:@types/ioredis@^4.28.10": - version "4.28.10" - resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff" - integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ== - dependencies: - "@types/node" "*" - -"@types/ioredis@^4.27.1": +"@types/ioredis4@npm:@types/ioredis@^4.28.10", "@types/ioredis@^4.27.1": version "4.28.10" resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff" integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ== @@ -13985,10 +13970,10 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== -better-sqlite3@8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-8.7.0.tgz#bcc341856187b1d110a8a47234fa89c48c8ef538" - integrity sha512-99jZU4le+f3G6aIl6PmmV0cxUIWqKieHxsiF7G34CVFiE+/UabpYqkU0NJIkY/96mQKikHeBjtR27vFfs5JpEw== +better-sqlite3@9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-9.5.0.tgz#0e10766cfb7f9b8916be3ab95ad9d5bcc4e6e6fd" + integrity sha512-01qVcM4gPNwE+PX7ARNiHINwzVuD6nx0gdldaAAcu+MrzyIAukQ31ZDKEpzRO/CNA9sHpxoTZ8rdjoyAin4dyg== dependencies: bindings "^1.5.0" prebuild-install "^7.1.1" @@ -14513,9 +14498,9 @@ bson@^5.5.0: integrity sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g== bson@^6.4.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/bson/-/bson-6.5.0.tgz#fc4828d065e64e48ea442b1a23099b2e52f7ff0b" - integrity sha512-DXf1BTAS8vKyR90BO4x5v3rKVarmkdkzwOrnYDFdjAY694ILNDkmA3uRh1xXJEl+C1DAh8XCvAQ+Gh3kzubtpg== + version "6.6.0" + resolved "https://registry.yarnpkg.com/bson/-/bson-6.6.0.tgz#f225137eb49fe19bee4d87949a0515c05176e2ad" + integrity sha512-BVINv2SgcMjL4oYbBuCQTpE3/VKOSxrOA8Cj/wQP7izSzlBGVomdm+TcUd0Pzy0ytLSSDweCKQ6X3f5veM5LQA== btoa-lite@^1.0.0: version "1.0.0" @@ -14552,11 +14537,6 @@ buffer-indexof-polyfill@~1.0.0: resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== -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-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -26371,10 +26351,10 @@ micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -mikro-orm@6.1.12: - version "6.1.12" - resolved "https://registry.yarnpkg.com/mikro-orm/-/mikro-orm-6.1.12.tgz#ee057b68d2f309420964807799556e8c52817763" - integrity sha512-pXpZ5dGMM0BBqYouU5EPuWkjWX/xnNzRVxsOTrKyyrm1ICTN7pawHn3UCVAggi+z1qVhpwxThGfZj+ZWD54duw== +mikro-orm@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/mikro-orm/-/mikro-orm-6.2.1.tgz#14088a75edaed1dd11042ed5cca9505ddb8aa27e" + integrity sha512-px/9uOzKoRmxDO150WOFndV1m6chjRc1UahM8Zhq8f/DnJ80qbVIhH9BKICu04A9l96SxVIL7AG94plFr8ZByg== miller-rabin@^4.0.0: version "4.0.1" @@ -27365,24 +27345,10 @@ mute-stream@1.0.0, mute-stream@^1.0.0, mute-stream@~1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== -mysql2@3.9.2: - version "3.9.2" - resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.9.2.tgz#567343581f9742032598b6c15bd7aa65d2f7d4af" - integrity sha512-3Cwg/UuRkAv/wm6RhtPE5L7JlPB877vwSF6gfLAS68H+zhH+u5oa3AieqEd0D0/kC3W7qIhYbH419f7O9i/5nw== - dependencies: - denque "^2.1.0" - generate-function "^2.3.1" - iconv-lite "^0.6.3" - long "^5.2.1" - lru-cache "^8.0.0" - named-placeholders "^1.1.3" - seq-queue "^0.0.5" - sqlstring "^2.3.2" - -mysql2@^3.9.3: - version "3.9.3" - resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.9.3.tgz#72a5e0c90d78ec2d8f9846e83727067c0cc8c25e" - integrity sha512-+ZaoF0llESUy7BffccHG+urErHcWPZ/WuzYAA9TEeLaDYyke3/3D+VQDzK9xzRnXpd0eMtRf0WNOeo4Q1Baung== +mysql2@3.9.4, mysql2@^3.9.3: + version "3.9.4" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.9.4.tgz#0d4f666015f8ed213aa6f2f5e59065eb99d7c3de" + integrity sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg== dependencies: denque "^2.1.0" generate-function "^2.3.1" @@ -29337,11 +29303,6 @@ package-json@^6.3.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== - pacote@15.2.0: version "15.2.0" resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.2.0.tgz#0f0dfcc3e60c7b39121b2ac612bf8596e95344d3" @@ -30019,11 +29980,16 @@ pg-connection-string@2.6.2: resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.2.tgz#713d82053de4e2bd166fab70cd4f26ad36aab475" integrity sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA== -pg-connection-string@^2.6.1, pg-connection-string@^2.6.2, pg-connection-string@^2.6.3: +pg-connection-string@^2.6.1, pg-connection-string@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.3.tgz#0393c04b00262bca1d656694f9193aaba9f17fb4" integrity sha512-77FxhhKJQH+xJx6tDqkhhMa0nZvv3U1HYLDQgwZxZafVD583++O5LXn5oo5HaQZ0vXwYcZA1koYAJM3JvD6Gtw== +pg-connection-string@^2.6.4: + 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-cursor@^2.10.4: version "2.10.4" resolved "https://registry.yarnpkg.com/pg-cursor/-/pg-cursor-2.10.4.tgz#4555a900ca93dbe5b6469df37cf0666ad381679b" @@ -30039,12 +30005,12 @@ pg-numeric@1.0.2: resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== -pg-pool@^3.6.1, pg-pool@^3.6.2: +pg-pool@^3.6.2: 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@*, pg-protocol@^1.6.0, pg-protocol@^1.6.1: +pg-protocol@*, pg-protocol@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== @@ -30080,16 +30046,14 @@ pg-types@^4.0.1: postgres-interval "^3.0.0" postgres-range "^1.1.1" -pg@8.11.3: - version "8.11.3" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.3.tgz#d7db6e3fe268fcedd65b8e4599cda0b8b4bf76cb" - integrity sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g== - dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "^2.6.2" - pg-pool "^3.6.1" - pg-protocol "^1.6.0" +pg@8.11.5: + version "8.11.5" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.5.tgz#e722b0a5f1ed92931c31758ebec3ddf878dd4128" + integrity sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw== + dependencies: + pg-connection-string "^2.6.4" + pg-pool "^3.6.2" + pg-protocol "^1.6.1" pg-types "^2.1.0" pgpass "1.x" optionalDependencies: @@ -32074,21 +32038,16 @@ ref-struct-di@^1.1.0: dependencies: debug "^3.1.0" -reflect-metadata@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.1.tgz#8d5513c0f5ef2b4b9c3865287f3c0940c1f67f74" - integrity sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw== +reflect-metadata@0.2.2, reflect-metadata@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== reflect-metadata@^0.1.13, reflect-metadata@^0.1.2: version "0.1.14" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== -reflect-metadata@^0.2.1: - version "0.2.2" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" - integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== - regenerate-unicode-properties@^10.1.0: version "10.1.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" @@ -34152,7 +34111,7 @@ string-similarity@^4.0.1: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.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.2, 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== @@ -34170,15 +34129,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.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.2, 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@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -34271,7 +34221,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", 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== @@ -34299,13 +34249,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: 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" @@ -37114,7 +37057,7 @@ wordwrap@~0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.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== @@ -37149,15 +37092,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.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"