diff --git a/.vscode/settings.json b/.vscode/settings.json index 57017121..45a53595 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,15 @@ "editor.formatOnSave": true }, "eslint.format.enable": true, - "eslint.lintTask.enable": true + "eslint.lintTask.enable": true, + "eslint.workingDirectories": [ + { + "directory": "./client", + "changeProcessCWD": true + }, + { + "directory": "./server", + "changeProcessCWD": true + } + ], } diff --git a/client/src/components/Nav.svelte b/client/src/components/Nav.svelte index 0f6cb9f9..6a0df5b1 100644 --- a/client/src/components/Nav.svelte +++ b/client/src/components/Nav.svelte @@ -157,12 +157,17 @@ class="w-full" href="human_resources/meal_tickets">{$_('human_resources.meal_tickets.title')} -
  • +
  • {$_('human_resources.meal_tickets.title')}
  • -
  • +
  • + {$_('human_resources.users.title')} +
  • +
  • {$_('human_resources.users.title')} diff --git a/client/src/normalizer/time.js b/client/src/normalizer/time.js index 16d86d68..5a610152 100644 --- a/client/src/normalizer/time.js +++ b/client/src/normalizer/time.js @@ -2,11 +2,11 @@ export const minutesToHours = (value) => { const hours = Math.floor(value / 60); const minutes = value % 60; - if (0 === hours) { + if (hours === 0) { return `${value}m`; } - if (0 === minutes) { + if (minutes === 0) { return `${hours}h`; } diff --git a/client/src/routes/crm/projects/_Table.svelte b/client/src/routes/crm/projects/_Table.svelte index 7e7d8985..fc4a66e7 100644 --- a/client/src/routes/crm/projects/_Table.svelte +++ b/client/src/routes/crm/projects/_Table.svelte @@ -21,6 +21,7 @@ {#each items as { id, name, dayDuration, customer, invoiceUnit } (id)} {name} + {customer.name} {$_(`crm.projects.invoice_unit.${invoiceUnit}`)} diff --git a/docker-compose.yml b/docker-compose.yml index 55d4e41b..5981f7b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,7 +42,7 @@ services: nginx: image: nginx:latest ports: - - 80:80 + - 81:80 depends_on: - 'client' - 'api' diff --git a/server/package-lock.json b/server/package-lock.json index 7e997e64..dcaacb44 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -751,11 +751,6 @@ }, "engines": { "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } } }, "node_modules/@eslint/eslintrc/node_modules/globals": { @@ -768,9 +763,6 @@ }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { @@ -799,9 +791,6 @@ "dev": true, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { @@ -1310,7 +1299,6 @@ "version": "7.6.15", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-7.6.15.tgz", "integrity": "sha512-8CrL/iY5Gt4HJfyDg1PgPalhT7tVRT643f2mGMgPum/P/e94uuwEYBNIgsMEVOJUrOAWZkNIN60uEf8JkH6GWw==", - "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.0.7", @@ -1961,19 +1949,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^4.0.0", - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { @@ -1986,11 +1961,6 @@ }, "engines": { "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { @@ -2029,13 +1999,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" } }, "node_modules/@typescript-eslint/parser": { @@ -2051,18 +2014,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, "node_modules/@typescript-eslint/parser/node_modules/debug": { @@ -2075,11 +2026,6 @@ }, "engines": { "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } } }, "node_modules/@typescript-eslint/parser/node_modules/ms": { @@ -2099,10 +2045,6 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/types": { @@ -2112,10 +2054,6 @@ "dev": true, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/typescript-estree": { @@ -2134,15 +2072,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { @@ -2155,11 +2084,6 @@ }, "engines": { "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { @@ -2194,10 +2118,6 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@webassemblyjs/ast": { @@ -2419,10 +2339,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } + "dev": true }, "node_modules/acorn-walk": { "version": "6.2.0", @@ -2639,9 +2556,6 @@ }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array-union": { @@ -2674,9 +2588,6 @@ }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/asn1": { @@ -3197,9 +3108,6 @@ "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/callsites": { @@ -4242,9 +4150,6 @@ }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/es-module-lexer": { @@ -4265,9 +4170,6 @@ }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/escalade": { @@ -4373,9 +4275,6 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-config-prettier": { @@ -4385,9 +4284,6 @@ "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" } }, "node_modules/eslint-import-resolver-node": { @@ -4514,9 +4410,6 @@ }, "engines": { "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { @@ -4667,9 +4560,6 @@ }, "engines": { "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" } }, "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { @@ -4727,11 +4617,6 @@ }, "engines": { "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } } }, "node_modules/eslint/node_modules/globals": { @@ -4744,9 +4629,6 @@ }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/js-yaml": { @@ -4841,9 +4723,6 @@ "dev": true, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/type-check": { @@ -4865,9 +4744,6 @@ "dev": true, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/espree": { @@ -5641,9 +5517,6 @@ "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-package-type": { @@ -5743,9 +5616,6 @@ }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globby/node_modules/ignore": { @@ -5827,10 +5697,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "dev": true }, "node_modules/has-flag": { "version": "4.0.0", @@ -5847,9 +5714,6 @@ "dev": true, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-unicode": { @@ -6289,10 +6153,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "dev": true }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -6316,9 +6177,6 @@ }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-buffer": { @@ -6334,9 +6192,6 @@ "dev": true, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-ci": { @@ -6391,9 +6246,6 @@ "dev": true, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-descriptor": { @@ -6498,9 +6350,6 @@ "dev": true, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-number": { @@ -6519,9 +6368,6 @@ "dev": true, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-plain-object": { @@ -6547,9 +6393,6 @@ }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-stream": { @@ -6568,9 +6411,6 @@ "dev": true, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-symbol": { @@ -6583,9 +6423,6 @@ }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-typedarray": { @@ -8818,10 +8655,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "dev": true }, "node_modules/object-keys": { "version": "1.1.1", @@ -8857,9 +8691,6 @@ }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.pick": { @@ -8887,9 +8718,6 @@ }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/on-finished": { @@ -9542,21 +9370,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "dev": true }, "node_modules/randombytes": { "version": "2.1.0", @@ -9995,20 +9809,6 @@ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "dependencies": { "queue-microtask": "^1.2.2" } @@ -10547,9 +10347,6 @@ }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/slice-ansi/node_modules/astral-regex": { @@ -11009,9 +10806,6 @@ "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { @@ -11022,9 +10816,6 @@ "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/strip-ansi": { @@ -11237,10 +11028,6 @@ "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/table/node_modules/ansi-regex": { @@ -11795,9 +11582,6 @@ }, "engines": { "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, "node_modules/tsutils/node_modules/tslib": { @@ -11956,9 +11740,6 @@ "has-bigints": "^1.0.1", "has-symbols": "^1.0.2", "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/union-value": { @@ -12349,9 +12130,6 @@ "is-number-object": "^1.0.4", "is-string": "^1.0.5", "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-module": { @@ -14693,8 +14471,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true, - "requires": {} + "dev": true }, "acorn-walk": { "version": "6.2.0", @@ -16410,8 +16187,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.2.0.tgz", "integrity": "sha512-dWV9EVeSo2qodOPi1iBYU/x6F6diHv8uujxbxr77xExs3zTAlNXvVZKiyLsQGNz7yPV2K49JY5WjPzNIuDc2Bw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.4", diff --git a/server/src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQuery.ts b/server/src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQuery.ts new file mode 100644 index 00000000..95cfa2a4 --- /dev/null +++ b/server/src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQuery.ts @@ -0,0 +1,6 @@ +import { IQuery } from 'src/Application/IQuery'; +import { User } from 'src/Domain/HumanResource/User/User.entity'; + +export class CountMealTicketPerMonthQuery implements IQuery { + constructor(public readonly user: User, public readonly currentDate: Date) {} +} diff --git a/server/src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQueryHandler.spec.ts b/server/src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQueryHandler.spec.ts new file mode 100644 index 00000000..06a60615 --- /dev/null +++ b/server/src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQueryHandler.spec.ts @@ -0,0 +1,128 @@ +import { mock, instance, when } from 'ts-mockito'; +import { User } from 'src/Domain/HumanResource/User/User.entity'; +import { MealTicketRemovalRepository } from 'src/Infrastructure/HumanResource/MealTicket/Repository/MealTicketRemovalRepository'; +import { CountMealTicketPerMonthQueryHandler } from './CountMealTicketPerMonthQueryHandler'; +import { CountMealTicketPerMonthQuery } from './CountMealTicketPerMonthQuery'; +import { DateUtilsAdapter } from 'src/Infrastructure/Adapter/DateUtilsAdapter'; +import { MealTicketRemoval } from 'src/Domain/HumanResource/MealTicket/MealTicketRemoval.entity'; +import { WorkingDayOfYearByMonth } from 'src/Domain/HumanResource/MealTicket/Strategy/WorkingDayOfYearByMonth'; +import { MealTicketSummaryView } from '../Views/MealTicketSummaryView'; + +describe('CountMealTicketPerMonthQueryHandler', () => { + let mealTicketRemovalRepository: MealTicketRemovalRepository; + let handler: CountMealTicketPerMonthQueryHandler; + let dateUtilsAdapter: DateUtilsAdapter; + + const now = new Date(); + + const user = mock(User); + + const command = new CountMealTicketPerMonthQuery(instance(user), now); + + beforeEach(() => { + mealTicketRemovalRepository = mock(MealTicketRemovalRepository); + dateUtilsAdapter = mock(DateUtilsAdapter); + + handler = new CountMealTicketPerMonthQueryHandler( + instance(dateUtilsAdapter), + instance(mealTicketRemovalRepository) + ); + }); + + it('should return the MealTicket Count for each month of the current year', async () => { + const workingDayByMonth1 = new WorkingDayOfYearByMonth(1, 20); + + const workingDayByMonth2 = new WorkingDayOfYearByMonth(2, 21); + + const workingDayByMonth3 = new WorkingDayOfYearByMonth(3, 23); + + const mealTicketsExceptions = [ + /* + There are 2 meal exceptions for the month of January + */ + { + count: 2, + date: new Date('1998-03-01') + } + ]; + + const expectedResult: MealTicketSummaryView[] = [ + new MealTicketSummaryView(1, 20, 0, 20), + new MealTicketSummaryView(2, 21, 0, 21), + new MealTicketSummaryView(3, 23, 2, 21) + ]; + + const ticketRemoval1 = mock(MealTicketRemoval); + when(ticketRemoval1.getDate()).thenReturn('2021-12-12'); + const ticketRemoval2 = mock(MealTicketRemoval); + when(ticketRemoval2.getDate()).thenReturn('2021-11-12'); + when(dateUtilsAdapter.getAllWorkingDayOfYearByMonth(now)).thenReturn([ + workingDayByMonth1, + workingDayByMonth2, + workingDayByMonth3 + ]); + + when( + mealTicketRemovalRepository.getAllByUserGroupedByMonth(instance(user)) + ).thenResolve(mealTicketsExceptions); + + expect(await handler.execute(command)).toStrictEqual(expectedResult); + }); + + it('should return a minimum of 0 meal ticket even if there are more exceptions', async () => { + const workingDayByMonth1 = new WorkingDayOfYearByMonth(1, 20); + + const workingDayByMonth2 = new WorkingDayOfYearByMonth(2, 21); + + const workingDayByMonth3 = new WorkingDayOfYearByMonth(3, 23); + + const mealTicketsExceptions = [ + /* + There is 1 meal exceptions for the month of january + */ + { + count: 1, + date: new Date('1998-01-01') + }, + + /* + There are 2 meal exceptions for the month of February + */ + + { + count: 2, + date: new Date('1998-02-01') + }, + + /* + There are 6 meal exceptions for the month of March + */ + { + count: 6, + date: new Date('1998-03-01') + } + ]; + + const expectedResult: MealTicketSummaryView[] = [ + new MealTicketSummaryView(1, 20, 1, 19), + new MealTicketSummaryView(2, 21, 2, 19), + new MealTicketSummaryView(3, 23, 6, 17) + ]; + + const ticketRemoval1 = mock(MealTicketRemoval); + when(ticketRemoval1.getDate()).thenReturn('2021-12-12'); + const ticketRemoval2 = mock(MealTicketRemoval); + when(ticketRemoval2.getDate()).thenReturn('2021-11-12'); + when(dateUtilsAdapter.getAllWorkingDayOfYearByMonth(now)).thenReturn([ + workingDayByMonth1, + workingDayByMonth2, + workingDayByMonth3 + ]); + + when( + mealTicketRemovalRepository.getAllByUserGroupedByMonth(instance(user)) + ).thenResolve(mealTicketsExceptions); + + expect(await handler.execute(command)).toStrictEqual(expectedResult); + }); +}); diff --git a/server/src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQueryHandler.ts b/server/src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQueryHandler.ts new file mode 100644 index 00000000..5ecdb8a9 --- /dev/null +++ b/server/src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQueryHandler.ts @@ -0,0 +1,68 @@ +import { QueryHandler } from '@nestjs/cqrs'; +import { Inject } from '@nestjs/common'; +import { IMealTicketRemovalRepository } from 'src/Domain/HumanResource/MealTicket/Repository/IMealTicketRemovalRepository'; +import { CountMealTicketPerMonthQuery } from './CountMealTicketPerMonthQuery'; +import { IDateUtils } from 'src/Application/IDateUtils'; +import { AvailableMealTicketStrategy } from 'src/Domain/HumanResource/MealTicket/Strategy/AvailableMealTicketStrategy'; +import { getMonth } from 'date-fns'; +import { MealTicketGroupedByMonthSummary } from 'src/Domain/HumanResource/MealTicket/Strategy/MealTicketGroupedByMonthSummary'; +import { MealTicketRemovalSummaryDTO } from 'src/Infrastructure/HumanResource/MealTicket/DTO/MealTicketRemovalSummaryDTO'; +import { MealTicketSummaryView } from '../Views/MealTicketSummaryView'; + +@QueryHandler(CountMealTicketPerMonthQuery) +export class CountMealTicketPerMonthQueryHandler { + constructor( + @Inject('IDateUtils') + private readonly dateUtils: IDateUtils, + @Inject('IMealTicketRemovalRepository') + private readonly mealTicketRemovalRepository: IMealTicketRemovalRepository + ) {} + + private buildMealTicketSummary = ( + mealTicketRemovals: MealTicketRemovalSummaryDTO[], + mealTicketGroupedByMonthSummaries: MealTicketGroupedByMonthSummary[] + ): MealTicketSummaryView[] => { + return mealTicketGroupedByMonthSummaries.map(summary => { + const month = summary.month; + const base = summary.mealTicketCount; + let total = base; + + const foundTicketRemoval = mealTicketRemovals.find(item => { + const _month = getMonth(item.date) + 1; + return summary.month === _month; + }); + + if (foundTicketRemoval) { + total = total - foundTicketRemoval.count; + } + + return new MealTicketSummaryView( + month, + base, + foundTicketRemoval ? foundTicketRemoval.count : 0, + total + ); + }); + }; + + public async execute( + command: CountMealTicketPerMonthQuery + ): Promise { + const { user, currentDate } = command; + const workingDaysByMonth = this.dateUtils.getAllWorkingDayOfYearByMonth( + currentDate + ); + const yearlyAvailableMealTickets = AvailableMealTicketStrategy.getMealTicketCountForEachMonthOfTheYear( + workingDaysByMonth + ); + + const mealTicketRemovals = await this.mealTicketRemovalRepository.getAllByUserGroupedByMonth( + user + ); + + return this.buildMealTicketSummary( + mealTicketRemovals, + yearlyAvailableMealTickets + ); + } +} diff --git a/server/src/Application/HumanResource/MealTicket/Views/MealTicketSummaryView.ts b/server/src/Application/HumanResource/MealTicket/Views/MealTicketSummaryView.ts new file mode 100644 index 00000000..d06f0cb5 --- /dev/null +++ b/server/src/Application/HumanResource/MealTicket/Views/MealTicketSummaryView.ts @@ -0,0 +1,8 @@ +export class MealTicketSummaryView { + constructor( + public readonly month: number, + public readonly base: number, + public readonly mealTicketRemovalCount: number, + public readonly total: number + ) {} +} diff --git a/server/src/Application/IDateUtils.ts b/server/src/Application/IDateUtils.ts index 38937b00..4d5537a6 100644 --- a/server/src/Application/IDateUtils.ts +++ b/server/src/Application/IDateUtils.ts @@ -1,3 +1,5 @@ +import { WorkingDayOfYearByMonth } from 'src/Domain/HumanResource/MealTicket/Strategy/WorkingDayOfYearByMonth'; + export interface IDateUtils { format(date: Date, format: string): string; getDaysInMonth(date: Date): number; @@ -14,4 +16,8 @@ export interface IDateUtils { isEndsAllDay: boolean ): number; addDaysToDate(date: Date, days: number): Date; + getYear(date: Date): number; + getLastDayOfYear(date: Date): Date; + getFirstDayOfYear(date: Date): Date; + getAllWorkingDayOfYearByMonth(date: Date): WorkingDayOfYearByMonth[]; } diff --git a/server/src/Domain/HumanResource/MealTicket/Repository/IMealTicketRemovalRepository.ts b/server/src/Domain/HumanResource/MealTicket/Repository/IMealTicketRemovalRepository.ts index d41d08c6..f2ec4717 100644 --- a/server/src/Domain/HumanResource/MealTicket/Repository/IMealTicketRemovalRepository.ts +++ b/server/src/Domain/HumanResource/MealTicket/Repository/IMealTicketRemovalRepository.ts @@ -1,3 +1,4 @@ +import { MealTicketRemovalSummaryDTO } from 'src/Infrastructure/HumanResource/MealTicket/DTO/MealTicketRemovalSummaryDTO'; import { User } from '../../User/User.entity'; import { MealTicketRemoval } from '../MealTicketRemoval.entity'; @@ -7,4 +8,7 @@ export interface IMealTicketRemovalRepository { user: User, date: Date ): Promise; + getAllByUserGroupedByMonth( + user: User + ): Promise; } diff --git a/server/src/Domain/HumanResource/MealTicket/Strategy/AvailableMealTicketStrategy.spec.ts b/server/src/Domain/HumanResource/MealTicket/Strategy/AvailableMealTicketStrategy.spec.ts new file mode 100644 index 00000000..03cb8d06 --- /dev/null +++ b/server/src/Domain/HumanResource/MealTicket/Strategy/AvailableMealTicketStrategy.spec.ts @@ -0,0 +1,19 @@ +import { WorkingDayOfYearByMonth } from 'src/Domain/HumanResource/MealTicket/Strategy/WorkingDayOfYearByMonth'; +import { AvailableMealTicketStrategy } from './AvailableMealTicketStrategy'; +import { MealTicketGroupedByMonthSummary } from './MealTicketGroupedByMonthSummary'; + +describe('AvailableMealTicketStrategy', () => { + it('testGetMealTicketCountForEachMonthOfTheYear', () => { + const workedDaysOfYearByMonth1 = new WorkingDayOfYearByMonth(1); + + workedDaysOfYearByMonth1.addOneWorkingDay(); + + const expectedResult = new MealTicketGroupedByMonthSummary(1, 1); + + expect( + AvailableMealTicketStrategy.getMealTicketCountForEachMonthOfTheYear([ + workedDaysOfYearByMonth1 + ]) + ).toEqual([expectedResult]); + }); +}); diff --git a/server/src/Domain/HumanResource/MealTicket/Strategy/AvailableMealTicketStrategy.ts b/server/src/Domain/HumanResource/MealTicket/Strategy/AvailableMealTicketStrategy.ts new file mode 100644 index 00000000..bdc0cddf --- /dev/null +++ b/server/src/Domain/HumanResource/MealTicket/Strategy/AvailableMealTicketStrategy.ts @@ -0,0 +1,15 @@ +import { WorkingDayOfYearByMonth } from './WorkingDayOfYearByMonth'; +import { MealTicketGroupedByMonthSummary } from './MealTicketGroupedByMonthSummary'; + +export class AvailableMealTicketStrategy { + public static getMealTicketCountForEachMonthOfTheYear( + workingDayOfYearByMonth: WorkingDayOfYearByMonth[] + ): MealTicketGroupedByMonthSummary[] { + return workingDayOfYearByMonth.map(item => { + return new MealTicketGroupedByMonthSummary( + item.month, + item.workingDaysCount + ); + }); + } +} diff --git a/server/src/Domain/HumanResource/MealTicket/Strategy/MealTicketGroupedByMonthSummary.spec.ts b/server/src/Domain/HumanResource/MealTicket/Strategy/MealTicketGroupedByMonthSummary.spec.ts new file mode 100644 index 00000000..7939496b --- /dev/null +++ b/server/src/Domain/HumanResource/MealTicket/Strategy/MealTicketGroupedByMonthSummary.spec.ts @@ -0,0 +1,9 @@ +import { MealTicketGroupedByMonthSummary } from './MealTicketGroupedByMonthSummary'; + +describe('MealTicketGroupedByMonthSummary', () => { + it('testShouldUpdate', () => { + const summary = new MealTicketGroupedByMonthSummary(1, 3); + summary.setMealTicketCount(5); + expect(summary.mealTicketCount).toBe(5); + }); +}); diff --git a/server/src/Domain/HumanResource/MealTicket/Strategy/MealTicketGroupedByMonthSummary.ts b/server/src/Domain/HumanResource/MealTicket/Strategy/MealTicketGroupedByMonthSummary.ts new file mode 100644 index 00000000..96932892 --- /dev/null +++ b/server/src/Domain/HumanResource/MealTicket/Strategy/MealTicketGroupedByMonthSummary.ts @@ -0,0 +1,11 @@ +export class MealTicketGroupedByMonthSummary { + month: number; + mealTicketCount: number; + constructor(month: number, mealTicketCount: number) { + this.month = month; + this.mealTicketCount = mealTicketCount; + } + setMealTicketCount(count: number) { + this.mealTicketCount = count; + } +} diff --git a/server/src/Domain/HumanResource/MealTicket/Strategy/WorkingDayOfYearByMonth.ts b/server/src/Domain/HumanResource/MealTicket/Strategy/WorkingDayOfYearByMonth.ts new file mode 100644 index 00000000..0906f76a --- /dev/null +++ b/server/src/Domain/HumanResource/MealTicket/Strategy/WorkingDayOfYearByMonth.ts @@ -0,0 +1,12 @@ +export class WorkingDayOfYearByMonth { + readonly month: number; + workingDaysCount: number; + constructor(month: number, workedDaysOfYear = 0) { + this.month = month; + this.workingDaysCount = workedDaysOfYear; + } + + public addOneWorkingDay() { + this.workingDaysCount += 1; + } +} diff --git a/server/src/Infrastructure/Accounting/accounting.module.ts b/server/src/Infrastructure/Accounting/accounting.module.ts index 70651b0c..e20eeb38 100644 --- a/server/src/Infrastructure/Accounting/accounting.module.ts +++ b/server/src/Infrastructure/Accounting/accounting.module.ts @@ -83,7 +83,6 @@ import { GetInvoicesQueryHandler } from 'src/Application/Accounting/Query/Invoic { provide: 'IEventRepository', useClass: EventRepository }, { provide: 'IInvoiceRepository', useClass: InvoiceRepository }, { provide: 'IInvoiceItemRepository', useClass: InvoiceItemRepository }, - Date, CreateQuoteCommandHandler, CreateQuoteItemsCommandHandler, CreateDailyRateCommandHandler, diff --git a/server/src/Infrastructure/Adapter/DateUtilsAdapter.spec.ts b/server/src/Infrastructure/Adapter/DateUtilsAdapter.spec.ts index 1625813e..05cfc61a 100644 --- a/server/src/Infrastructure/Adapter/DateUtilsAdapter.spec.ts +++ b/server/src/Infrastructure/Adapter/DateUtilsAdapter.spec.ts @@ -121,4 +121,36 @@ describe('DateUtilsAdapter', () => { dateUtils.getLeaveDuration('2020-05-05', false, '2020-05-05', false) ).toBe(0.5); }); + + it('testGetYear', () => { + const dateUtils = new DateUtilsAdapter(); + + expect(dateUtils.getYear(new Date('2020-05-05'))).toBe(2020); + }); + + it('testGetAllWorkingDayOfYearByMonth', () => { + const dateUtils = new DateUtilsAdapter(); + const now = new Date('2021-12-12'); + const result = dateUtils.getAllWorkingDayOfYearByMonth(now); + expect(result.length).toBe(12); + + const WorkingDaysOfMarch = result.find(item => { + return item.month === 3; + }); + expect(WorkingDaysOfMarch.workingDaysCount).toBe(23); + }); + + it('testGetLastDayOfYear', () => { + const dateUtils = new DateUtilsAdapter(); + const now = new Date('2021-12-12'); + const result = dateUtils.getLastDayOfYear(now); + expect(result).toStrictEqual(new Date('2021-12-31')); + }); + + it('getFirstDayOfYear', () => { + const dateUtils = new DateUtilsAdapter(); + const now = new Date('2021-12-12'); + const result = dateUtils.getFirstDayOfYear(now); + expect(result).toStrictEqual(new Date('2021-01-01')); + }); }); diff --git a/server/src/Infrastructure/Adapter/DateUtilsAdapter.ts b/server/src/Infrastructure/Adapter/DateUtilsAdapter.ts index 31dba892..32e20a6c 100644 --- a/server/src/Infrastructure/Adapter/DateUtilsAdapter.ts +++ b/server/src/Infrastructure/Adapter/DateUtilsAdapter.ts @@ -4,9 +4,11 @@ import { isWeekend as fnsIsWeekend, getDaysInMonth as fnsGetDaysInMonth, eachDayOfInterval, - addDays + addDays, + getYear } from 'date-fns'; import { IDateUtils } from 'src/Application/IDateUtils'; +import { WorkingDayOfYearByMonth } from 'src/Domain/HumanResource/MealTicket/Strategy/WorkingDayOfYearByMonth'; @Injectable() export class DateUtilsAdapter implements IDateUtils { @@ -26,6 +28,18 @@ export class DateUtilsAdapter implements IDateUtils { return new Date(); } + public getYear(date: Date): number { + return getYear(date); + } + + public getLastDayOfYear(date: Date): Date { + return new Date(`${getYear(date)}/12/31`); + } + + public getFirstDayOfYear(date: Date): Date { + return new Date(`${getYear(date)}/01/01`); + } + public getCurrentDateToISOString(): string { return this.getCurrentDate().toISOString(); } @@ -57,6 +71,32 @@ export class DateUtilsAdapter implements IDateUtils { return dates; } + public getAllWorkingDayOfYearByMonth(date: Date): WorkingDayOfYearByMonth[] { + const lastDayOfYear = this.getLastDayOfYear(date); + const firstDayOfYear = this.getFirstDayOfYear(date); + + const workedDaysOfYear = this.getWorkedDaysDuringAPeriod( + firstDayOfYear, + lastDayOfYear + ); + + const defaultValues: WorkingDayOfYearByMonth[] = []; + + return workedDaysOfYear.reduce((prev, next) => { + const currentMonth = next.getMonth() + 1; + const itemWithMonth = prev.find(item => item.month === currentMonth); + + if (itemWithMonth) { + itemWithMonth.addOneWorkingDay(); + return prev; + } + const working = new WorkingDayOfYearByMonth(currentMonth); + working.addOneWorkingDay(); + + return [...prev, working]; + }, defaultValues); + } + public getWorkedFreeDays(year: number): Date[] { const fixedDays: Date[] = [ new Date(`${year}-01-01`), // New Year's Day diff --git a/server/src/Infrastructure/HumanResource/MealTicket/Action/GetAvailableMealTicketsAction.ts b/server/src/Infrastructure/HumanResource/MealTicket/Action/GetAvailableMealTicketsAction.ts new file mode 100644 index 00000000..98277780 --- /dev/null +++ b/server/src/Infrastructure/HumanResource/MealTicket/Action/GetAvailableMealTicketsAction.ts @@ -0,0 +1,40 @@ +import { + Get, + Controller, + Inject, + BadRequestException, + UseGuards +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { User, UserRole } from 'src/Domain/HumanResource/User/User.entity'; +import { RolesGuard } from 'src/Infrastructure/HumanResource/User/Security/RolesGuard'; +import { Roles } from 'src/Infrastructure/HumanResource/User/Decorator/Roles'; +import { LoggedUser } from '../../User/Decorator/LoggedUser'; +import { IQueryBus } from '@nestjs/cqrs'; +import { CountMealTicketPerMonthQuery } from 'src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQuery'; + +@Controller('meal-tickets') +@ApiTags('Human Resource') +@ApiBearerAuth() +@UseGuards(AuthGuard('bearer'), RolesGuard) +export class GetAvailableMealTicketsAction { + constructor( + @Inject('IQueryBus') + private readonly queryBus: IQueryBus + ) {} + + @Get('count') + @Roles(UserRole.COOPERATOR, UserRole.EMPLOYEE) + @ApiOperation({ summary: 'Get all available Meal Tickets' }) + public async index(@LoggedUser() user: User) { + try { + const result = await this.queryBus.execute( + new CountMealTicketPerMonthQuery(user, new Date()) + ); + return result; + } catch (e) { + throw new BadRequestException(e.message); + } + } +} diff --git a/server/src/Infrastructure/HumanResource/MealTicket/DTO/MealTicketRemovalSummaryDTO.ts b/server/src/Infrastructure/HumanResource/MealTicket/DTO/MealTicketRemovalSummaryDTO.ts new file mode 100644 index 00000000..cd2d5c74 --- /dev/null +++ b/server/src/Infrastructure/HumanResource/MealTicket/DTO/MealTicketRemovalSummaryDTO.ts @@ -0,0 +1,4 @@ +export class MealTicketRemovalSummaryDTO { + public date: Date; + public count: number; +} diff --git a/server/src/Infrastructure/HumanResource/MealTicket/Repository/MealTicketRemovalRepository.ts b/server/src/Infrastructure/HumanResource/MealTicket/Repository/MealTicketRemovalRepository.ts index f4612d9e..4f00756a 100644 --- a/server/src/Infrastructure/HumanResource/MealTicket/Repository/MealTicketRemovalRepository.ts +++ b/server/src/Infrastructure/HumanResource/MealTicket/Repository/MealTicketRemovalRepository.ts @@ -3,6 +3,7 @@ import { Repository } from 'typeorm'; import { MealTicketRemoval } from 'src/Domain/HumanResource/MealTicket/MealTicketRemoval.entity'; import { IMealTicketRemovalRepository } from 'src/Domain/HumanResource/MealTicket/Repository/IMealTicketRemovalRepository'; import { User } from 'src/Domain/HumanResource/User/User.entity'; +import { MealTicketRemovalSummaryDTO } from '../DTO/MealTicketRemovalSummaryDTO'; export class MealTicketRemovalRepository implements IMealTicketRemovalRepository { @@ -36,4 +37,15 @@ export class MealTicketRemovalRepository .andWhere('extract(day FROM mealTicketRemoval.date) = :day', { day }) .getOne(); } + + public getAllByUserGroupedByMonth( + user: User + ): Promise { + return this.repository + .createQueryBuilder('mealTicketRemoval') + .select(["date_trunc('month', date) date, count(id)"]) + .where('mealTicketRemoval.user = :userId', { userId: user.getId() }) + .groupBy('date') + .getRawMany(); + } } diff --git a/server/src/Infrastructure/HumanResource/humanResource.module.ts b/server/src/Infrastructure/HumanResource/humanResource.module.ts index 9f6c6efb..9cd17406 100644 --- a/server/src/Infrastructure/HumanResource/humanResource.module.ts +++ b/server/src/Infrastructure/HumanResource/humanResource.module.ts @@ -59,14 +59,16 @@ import { GetUserAction } from './User/Action/GetUserAction'; import { UpdateUserCommandHandler } from 'src/Application/HumanResource/User/Command/UpdateUserCommandHandler'; import { GetUserAdministrativeByIdQueryHandler } from 'src/Application/HumanResource/User/Query/GetUserAdministrativeByIdQueryHandler'; import { GetLeavesAction } from './Leave/Action/GetLeavesAction'; -import { CanLeaveRequestBeRemoved } from 'src/Domain/HumanResource/Leave/Specification/CanLeaveRequestBeRemoved'; -import { DeleteLeaveRequestCommandHandler } from 'src/Application/HumanResource/Leave/Command/DeleteLeaveRequestCommandHandler'; -import { DeleteLeaveRequestAction } from './Leave/Action/DeleteLeaveRequestAction'; import { MealTicketRemoval } from 'src/Domain/HumanResource/MealTicket/MealTicketRemoval.entity'; import { MealTicketRemovalRepository } from './MealTicket/Repository/MealTicketRemovalRepository'; import { IsMealTicketRemovalAlreadyExist } from 'src/Domain/HumanResource/MealTicket/Specification/IsMealTicketRemovalAlreadyExist'; import { CreateMealTicketRemovalCommandHandler } from 'src/Application/HumanResource/MealTicket/Command/CreateMealTicketRemovalCommandHandler'; import { CreateMealTicketRemovalAction } from './MealTicket/Action/CreateMealTicketRemovalAction'; +import { CanLeaveRequestBeRemoved } from 'src/Domain/HumanResource/Leave/Specification/CanLeaveRequestBeRemoved'; +import { DeleteLeaveRequestCommandHandler } from 'src/Application/HumanResource/Leave/Command/DeleteLeaveRequestCommandHandler'; +import { DeleteLeaveRequestAction } from './Leave/Action/DeleteLeaveRequestAction'; +import { GetAvailableMealTicketsAction } from './MealTicket/Action/GetAvailableMealTicketsAction'; +import { CountMealTicketPerMonthQueryHandler } from 'src/Application/HumanResource/MealTicket/Query/CountMealTicketPerMonthQueryHandler'; @Module({ imports: [ @@ -101,6 +103,8 @@ import { CreateMealTicketRemovalAction } from './MealTicket/Action/CreateMealTic CreateLeaveRequestAction, RefuseLeaveRequestAction, AcceptLeaveRequestAction, + CreateMealTicketRemovalAction, + GetAvailableMealTicketsAction, DeleteLeaveRequestAction, CreateMealTicketRemovalAction ], @@ -122,6 +126,11 @@ import { CreateMealTicketRemovalAction } from './MealTicket/Action/CreateMealTic provide: 'IUserAdministrativeRepository', useClass: UserAdministrativeRepository }, + { + provide: 'IMealTicketRemovalRepository', + useClass: MealTicketRemovalRepository + }, + Date, CreatePaySlipCommandHandler, IsPaySlipAlreadyExist, LoginQueryHandler, @@ -148,7 +157,8 @@ import { CreateMealTicketRemovalAction } from './MealTicket/Action/CreateMealTic CanLeaveRequestBeRemoved, DeleteLeaveRequestCommandHandler, IsMealTicketRemovalAlreadyExist, - CreateMealTicketRemovalCommandHandler + CreateMealTicketRemovalCommandHandler, + CountMealTicketPerMonthQueryHandler ] }) export class HumanResourceModule {}