From b74e939075d6d98da8ca516de89d5748e74638f2 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Thu, 29 Aug 2024 10:32:21 +0300 Subject: [PATCH 01/55] feat: [Contracts] Test PDF generation on server --- backend/nest-cli.json | 5 +- backend/package-lock.json | 878 +++++++++++++++++- backend/package.json | 1 + backend/src/api/public/public.controller.ts | 23 +- backend/src/common/helpers/pdf-from-html.ts | 11 + .../src/modules/documents/documents.module.ts | 4 + .../documents/services/pdf-generator.ts | 33 + .../modules/documents/templates/contract.hbs | 131 +++ .../documents/templates/partials/faq-tc.hbs | 10 + .../documents/templates/partials/footer.hbs | 84 ++ .../documents/templates/partials/header.hbs | 3 + .../documents/generate-pdfs.usecase.ts | 12 + backend/src/usecases/use-case.module.ts | 5 + 13 files changed, 1164 insertions(+), 36 deletions(-) create mode 100644 backend/src/common/helpers/pdf-from-html.ts create mode 100644 backend/src/modules/documents/services/pdf-generator.ts create mode 100644 backend/src/modules/documents/templates/contract.hbs create mode 100644 backend/src/modules/documents/templates/partials/faq-tc.hbs create mode 100644 backend/src/modules/documents/templates/partials/footer.hbs create mode 100644 backend/src/modules/documents/templates/partials/header.hbs create mode 100644 backend/src/usecases/documents/generate-pdfs.usecase.ts diff --git a/backend/nest-cli.json b/backend/nest-cli.json index 7c274a3af..a7dcbac32 100644 --- a/backend/nest-cli.json +++ b/backend/nest-cli.json @@ -4,7 +4,10 @@ "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true, - "assets": ["modules/mail/templates/**/*"], + "assets": [ + "modules/mail/templates/**/*", + "modules/documents/templates/**/*" + ], "plugins": [ { "name": "@nestjs/swagger/plugin", diff --git a/backend/package-lock.json b/backend/package-lock.json index 4969430fa..1c73e76d8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -47,6 +47,7 @@ "passport-jwt": "^4.0.1", "pg": "^8.12.0", "pino-http": "^10.2.0", + "puppeteer": "^23.2.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.20", @@ -1181,7 +1182,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dev": true, "dependencies": { "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" @@ -1432,7 +1432,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "devOptional": true, "engines": { "node": ">=6.9.0" } @@ -1463,7 +1462,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", @@ -1478,7 +1476,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -1490,7 +1487,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1504,7 +1500,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -1512,14 +1507,12 @@ "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/highlight/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -1528,7 +1521,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -3446,6 +3438,48 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", + "dependencies": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -4190,6 +4224,11 @@ "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -4578,6 +4617,15 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", @@ -5086,6 +5134,38 @@ "node": ">=0.8" } }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -5276,6 +5356,17 @@ "integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==", "optional": true }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", @@ -5304,6 +5395,11 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -5437,6 +5533,47 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", + "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.2.0.tgz", + "integrity": "sha512-+o9MG5bPRRBlkVSpfFlMag3n7wMaIZb4YZasU2+/96f+3HTQ4F9DKQeu3K/Sjz1W0umu6xvVq1ON0ipWdMlr3A==", + "optional": true, + "dependencies": { + "streamx": "^2.18.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5456,6 +5593,14 @@ } ] }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5604,7 +5749,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -5624,6 +5768,14 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -5890,6 +6042,19 @@ "node": ">=6.0" } }, + "node_modules/chromium-bidi": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.4.tgz", + "integrity": "sha512-8zoq6ogmhQQkAKZVKO2ObFTl4uOkqoX1PlKQX3hZQ5E9cbUotcAb7h4pTNVAGGv8Z36PF3CtdOriEp/Rz82JqQ==", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -6436,6 +6601,14 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, "node_modules/date-fns": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", @@ -6523,6 +6696,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6589,6 +6775,11 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "optional": true }, + "node_modules/devtools-protocol": { + "version": "0.0.1330662", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1330662.tgz", + "integrity": "sha512-pzh6YQ8zZfz3iKlCvgzVCu22NdpZ8hNmwU6WnQjNVquh0A9iVosPtNLWDwaWVGyrntQlltPFztTMK5Cg6lfCuw==" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -6855,6 +7046,14 @@ "node": ">=8.10.0" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", @@ -6880,6 +7079,14 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -6889,7 +7096,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -6957,11 +7163,47 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -7276,7 +7518,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -7504,6 +7745,60 @@ "node": ">=4" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7515,6 +7810,11 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -7591,6 +7891,14 @@ "bser": "2.1.1" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -7968,6 +8276,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/glob": { "version": "10.3.12", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", @@ -8070,8 +8426,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -8299,6 +8654,72 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -8479,6 +8900,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -8490,8 +8928,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -9489,8 +9926,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -9503,6 +9939,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -9523,8 +9964,7 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -9559,7 +9999,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -9783,8 +10222,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/linkify-it": { "version": "5.0.0", @@ -10248,6 +10686,11 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, "node_modules/mjml": { "version": "4.15.3", "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.15.3.tgz", @@ -10806,6 +11249,14 @@ "pino-http": "^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -11163,6 +11614,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -11193,7 +11695,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -11364,6 +11865,11 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, "node_modules/pg": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", @@ -11448,8 +11954,7 @@ "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "4.0.1", @@ -11734,6 +12239,14 @@ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -11799,6 +12312,53 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -11928,6 +12488,15 @@ "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", "optional": true }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -11945,6 +12514,88 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.2.0.tgz", + "integrity": "sha512-MP7kLOdCfx1BJaGN5sgRo5fTYwAyGrlwWtrNphjKcwv/HO91+m90gbbwpRHbGl0rCvrmylq6vljn+zrjukniVg==", + "hasInstallScript": true, + "dependencies": { + "@puppeteer/browsers": "2.3.1", + "chromium-bidi": "0.6.4", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1330662", + "puppeteer-core": "23.2.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.2.0.tgz", + "integrity": "sha512-OFyPp2oolGSesx6ZrpmorE5tCaCKY1Z5e/h8f6sB0NpiezenB72jdWBdOrvBO/bUXyq14XyGJsDRUsv0ZOPdZA==", + "dependencies": { + "@puppeteer/browsers": "2.3.1", + "chromium-bidi": "0.6.4", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1330662", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/puppeteer/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -11994,6 +12645,11 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -12589,9 +13245,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -12753,6 +13409,62 @@ "node": "*" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/sonic-boom": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", @@ -12856,6 +13568,19 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.19.0.tgz", + "integrity": "sha512-5z6CNR4gtkPbwlxyEqoDGDmWIzoNJqCBt4Eac1ICP9YaIT08ct712cFj0u1rx4F8luAuL+3Qc+RFIdI4OX00kg==", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -13105,6 +13830,29 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/terser": { "version": "5.31.2", "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.2.tgz", @@ -13239,6 +13987,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13274,8 +14030,7 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, "node_modules/tlds": { "version": "1.252.0", @@ -13572,6 +14327,11 @@ "node": ">= 0.6" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==" + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -13793,6 +14553,15 @@ "node": ">=8" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -13802,7 +14571,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -13859,6 +14627,11 @@ "punycode": "^2.1.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -14311,6 +15084,26 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", @@ -14377,6 +15170,15 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -14396,6 +15198,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/backend/package.json b/backend/package.json index ba6aa9f44..5f43f9eba 100644 --- a/backend/package.json +++ b/backend/package.json @@ -64,6 +64,7 @@ "passport-jwt": "^4.0.1", "pg": "^8.12.0", "pino-http": "^10.2.0", + "puppeteer": "^23.2.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.20", diff --git a/backend/src/api/public/public.controller.ts b/backend/src/api/public/public.controller.ts index 47220c57d..d898e86e0 100644 --- a/backend/src/api/public/public.controller.ts +++ b/backend/src/api/public/public.controller.ts @@ -1,9 +1,13 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, Res } from '@nestjs/common'; import { SkipThrottle } from '@nestjs/throttler'; import { APP_VERSION } from 'src/common/constants/version'; +import { Response } from 'express'; +import { GeneratePDFsUseCase } from 'src/usecases/documents/generate-pdfs.usecase'; @Controller('public') export class PublicController { + constructor(private readonly generatePDFsUseCase: GeneratePDFsUseCase) {} + @SkipThrottle() @Get('health') healthCheck(): 'OK' { @@ -15,4 +19,21 @@ export class PublicController { version(): unknown { return APP_VERSION; } + + @Get('pdf') + async pdf(@Res() res: Response): Promise { + const buffer = await this.generatePDFsUseCase.execute(); + res.set({ + // pdf + 'Content-Type': 'application/pdf', + // 'Content-Disposition': 'attachment; filename=invoice.pdf', + 'Content-Disposition': 'inline; filename=invoice.pdf', + 'Content-Length': buffer.length, + // prevent cache + 'Cache-Control': 'no-cache, no-store, must-revalidate', + Pragma: 'no-cache', + Expires: 0, + }); + res.end(buffer); + } } diff --git a/backend/src/common/helpers/pdf-from-html.ts b/backend/src/common/helpers/pdf-from-html.ts new file mode 100644 index 000000000..fae9d707b --- /dev/null +++ b/backend/src/common/helpers/pdf-from-html.ts @@ -0,0 +1,11 @@ +import puppeteer from 'puppeteer'; + +export const HTMLtoPDF = async (html: string): Promise => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.setContent(html); + const buffer = await page.pdf({ format: 'A4' }); + await browser.close(); + + return buffer; +}; diff --git a/backend/src/modules/documents/documents.module.ts b/backend/src/modules/documents/documents.module.ts index 472a9c852..ca65a1f0a 100644 --- a/backend/src/modules/documents/documents.module.ts +++ b/backend/src/modules/documents/documents.module.ts @@ -6,6 +6,7 @@ import { TemplateFacade } from './services/template.facade'; import { ContractEntity } from './entities/contract.entity'; import { ContractRepositoryService } from './repositories/contract.repository'; import { ContractFacade } from './services/contract.facade'; +import { PDFGenerator } from './services/pdf-generator'; @Module({ imports: [TypeOrmModule.forFeature([TemplateEntity, ContractEntity])], @@ -16,11 +17,14 @@ import { ContractFacade } from './services/contract.facade'; // Facades TemplateFacade, ContractFacade, + // Services + PDFGenerator, ], exports: [ // Export only facades! TemplateFacade, ContractFacade, + PDFGenerator, ], }) export class DocumentsModule {} diff --git a/backend/src/modules/documents/services/pdf-generator.ts b/backend/src/modules/documents/services/pdf-generator.ts new file mode 100644 index 000000000..7f425c25e --- /dev/null +++ b/backend/src/modules/documents/services/pdf-generator.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { resolve } from 'path'; +import * as fs from 'fs'; +import { HTMLtoPDF } from 'src/common/helpers/pdf-from-html'; +import Handlebars from 'handlebars'; + +@Injectable() +export class PDFGenerator { + constructor() { + // TODO: This should be handled using a HandlebarsAdapter, similar to MailerModule to register partials/helpers/etc + const templateDir = resolve( + __dirname, + '..', + 'templates/partials', + 'header.hbs', + ); + const file = fs.readFileSync(templateDir, 'utf-8'); + const template = Handlebars.compile(file); + Handlebars.registerPartial('header', template); + } + + public async generatePDF(): Promise { + const templateDir = resolve(__dirname, '..', 'templates', 'contract.hbs'); + const file = fs.readFileSync(templateDir, 'utf-8'); + const template = Handlebars.compile(file); + const fileHTML = template({ + title: 'dadsa', + subtitle: `
Text
`, + }); + + return HTMLtoPDF(fileHTML); + } +} diff --git a/backend/src/modules/documents/templates/contract.hbs b/backend/src/modules/documents/templates/contract.hbs new file mode 100644 index 000000000..6a083d9f0 --- /dev/null +++ b/backend/src/modules/documents/templates/contract.hbs @@ -0,0 +1,131 @@ + + + + + Contract de Voluntariat + + + +
+

CONTRACT DE VOLUNTARIAT

+

{{title}}

+ +

Nr. + [Numar contract] + din data de + [Data contract]

+ +
+

Între + Asociația ZEN, cu sediul în Târgu-Jiu, + jud. Gorj, str. Sediului oficial, nr 1, bl 1, ap 19, identificată cu + CUI + RO100001, reprezentată prin + Tudorache Tudor, în calitate de + Director executiv numită în continuare + Organizația

+ +

și

+ +

[Nume si prenume voluntar], + domiciliat(ă) în + [Adresa domiciliu voluntar], C.N.P + [Numar CNP], legitimat cu BI/CI seria + [Serie] + nr. + [Numar], eliberat de + [Unitate eliberare], la data de + [Data eliberarii], numit în continuare + Voluntar,

+
+ +
+

s-a convenit încheierea prezentului contract în baza Legii nr. + 78/2014 privind reglementarea activității de voluntariat din România.

+ +

DURATA CONTRACTULUI

+ +

Prezentul contract se încheie pe o perioadă determinată, între data + de + [Data inceput] + și + [Data final]

+
+ +

Program de Activități

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ZiuaOraActivitateLocațiePriorități
Luni09:00 - 12:00Asistență administrativăBiroul centralMedie
14:00 - 17:00Organizare evenimenteSala de conferințeÎnaltă
Marți10:00 - 15:00Activități cu beneficiariiCentrul comunitarÎnaltă
Miercuri09:00 - 11:00Întâlnire echipăBiroul centralMedie
12:00 - 16:00Strângere de fonduriDiverse locațiiÎnaltă
Joi13:00 - 18:00Mentorat tineriȘcoala localăMedie
Vineri10:00 - 14:00Activități ecologiceParcul orașuluiScăzută
+
+ + \ No newline at end of file diff --git a/backend/src/modules/documents/templates/partials/faq-tc.hbs b/backend/src/modules/documents/templates/partials/faq-tc.hbs new file mode 100644 index 000000000..e14c18cea --- /dev/null +++ b/backend/src/modules/documents/templates/partials/faq-tc.hbs @@ -0,0 +1,10 @@ + +

If you have any questions, reach out to us at + hello@onghub.com. Need help? Check out + our + FAQs + and + Terms and conditions. +

\ No newline at end of file diff --git a/backend/src/modules/documents/templates/partials/footer.hbs b/backend/src/modules/documents/templates/partials/footer.hbs new file mode 100644 index 000000000..cdcef4a06 --- /dev/null +++ b/backend/src/modules/documents/templates/partials/footer.hbs @@ -0,0 +1,84 @@ +
+ + + + + +
+

+ Soluție proiectată, dezvoltată și administrată pro-bono de +

+
+ +
+ +
+ +

+ Dacă vrei să iei legătura cu noi o poți face pe e-mail la adresa: + {{contactEmail}} +

+ + + + + + + + +
+ + + + + + + + + + + + + + + +
+
+` \ No newline at end of file diff --git a/backend/src/modules/documents/templates/partials/header.hbs b/backend/src/modules/documents/templates/partials/header.hbs new file mode 100644 index 000000000..38a190f10 --- /dev/null +++ b/backend/src/modules/documents/templates/partials/header.hbs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/backend/src/usecases/documents/generate-pdfs.usecase.ts b/backend/src/usecases/documents/generate-pdfs.usecase.ts new file mode 100644 index 000000000..2fbafb87f --- /dev/null +++ b/backend/src/usecases/documents/generate-pdfs.usecase.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; +import { PDFGenerator } from 'src/modules/documents/services/pdf-generator'; + +@Injectable() +export class GeneratePDFsUseCase implements IUseCaseService { + constructor(private readonly pDFGenerator: PDFGenerator) {} + + public async execute(): Promise { + return this.pDFGenerator.generatePDF(); + } +} diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts index 3caad7105..12fa37910 100644 --- a/backend/src/usecases/use-case.module.ts +++ b/backend/src/usecases/use-case.module.ts @@ -136,6 +136,7 @@ import { GetOneRegularUserProfileUseCase } from './user/get-regule-user-profile. import { SyncUserOrganizationsUsecase } from './user/sync-user-organizations.usecase'; import { GetRejectedAccessRequestUsecase } from './access-request/get-rejected-access-request.usecase'; import { DeleteAccountRegularUserUsecase } from './user/delete-account.usecase'; +import { GeneratePDFsUseCase } from './documents/generate-pdfs.usecase'; @Module({ imports: [ @@ -297,6 +298,8 @@ import { DeleteAccountRegularUserUsecase } from './user/delete-account.usecase'; CancelContractUsecase, // Notifications UpdateSettingsUsecase, + // Testing PDFs + GeneratePDFsUseCase, ], exports: [ // Organization @@ -441,6 +444,8 @@ import { DeleteAccountRegularUserUsecase } from './user/delete-account.usecase'; GetVicStatisticsUsecase, // Notifications UpdateSettingsUsecase, + // Testing PDFs + GeneratePDFsUseCase, ], }) export class UseCaseModule {} From afbb42921768e673adeee477316014c95221a3c1 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Thu, 29 Aug 2024 10:42:28 +0300 Subject: [PATCH 02/55] feat: [Contracts] Configure puppeteer to run headless --- backend/src/common/helpers/pdf-from-html.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/common/helpers/pdf-from-html.ts b/backend/src/common/helpers/pdf-from-html.ts index fae9d707b..0d56de21b 100644 --- a/backend/src/common/helpers/pdf-from-html.ts +++ b/backend/src/common/helpers/pdf-from-html.ts @@ -1,7 +1,10 @@ import puppeteer from 'puppeteer'; export const HTMLtoPDF = async (html: string): Promise => { - const browser = await puppeteer.launch(); + const browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); const page = await browser.newPage(); await page.setContent(html); const buffer = await page.pdf({ format: 'A4' }); From 8bf7a3bdc46ec8506106e599599aec03c2acc7a1 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Thu, 29 Aug 2024 13:51:12 +0300 Subject: [PATCH 03/55] feat: [Contracts] - Update docker file to include Puppeteer and Chrome --- backend/Dockerfile | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 115a26875..3ea0c0a61 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,42 @@ -FROM --platform=linux/amd64 node:16.15.1-alpine As development +FROM --platform=linux/amd64 node:20@sha256:a4d1de4c7339eabcf78a90137dfd551b798829e3ef3e399e0036ac454afa1291 As development + +# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others) +# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer +# installs, work. +RUN apt-get update \ + && apt-get install -y wget gnupg \ + && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ + && apt-get update \ + && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +# If running Docker >= 1.13.0 use docker run's --init arg to reap zombie processes, otherwise +# uncomment the following lines to have `dumb-init` as PID 1 +# ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_x86_64 /usr/local/bin/dumb-init +# RUN chmod +x /usr/local/bin/dumb-init +# ENTRYPOINT ["dumb-init", "--"] + +# Uncomment to skip the chromium download when installing puppeteer. If you do, +# you'll need to launch puppeteer with: +# browser.launch({executablePath: 'google-chrome-stable'}) +# ENV PUPPETEER_SKIP_DOWNLOAD true + +# # Install puppeteer so it's available in the container. +# RUN npm init -y && \ +# npm i puppeteer \ +# # Add user so we don't need --no-sandbox. +# # same layer as npm install to keep re-chowned files from using up several hundred MBs more space +# && groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ +# && mkdir -p /home/pptruser/Downloads \ +# && chown -R pptruser:pptruser /home/pptruser \ +# && chown -R pptruser:pptruser /node_modules \ +# && chown -R pptruser:pptruser /package.json \ +# && chown -R pptruser:pptruser /package-lock.json + +# Run everything after as non-privileged user. +# USER pptruser WORKDIR /usr/src/app @@ -12,7 +50,7 @@ COPY . . RUN npm run build -FROM --platform=linux/amd64 node:16.15.1-alpine as production +FROM --platform=linux/amd64 node:20@sha256:a4d1de4c7339eabcf78a90137dfd551b798829e3ef3e399e0036ac454afa1291 as production ARG NODE_ENV=production ENV NODE_ENV=${NODE_ENV} @@ -25,4 +63,6 @@ RUN npm install --only=production COPY --from=development /usr/src/app/dist ./dist + + CMD ["node", "dist/main"] \ No newline at end of file From c0289abd214ba691f6e7ec75aabe834d3e78fe2c Mon Sep 17 00:00:00 2001 From: luciatugui Date: Fri, 30 Aug 2024 18:52:56 +0300 Subject: [PATCH 04/55] wip: contract template page --- frontend/package-lock.json | 163 ++++++++++++++++-- frontend/package.json | 2 + frontend/src/assets/images/signature.svg | 10 ++ .../src/assets/locales/ro/translation.json | 104 +++++++++++ frontend/src/common/constants/routes.ts | 5 + frontend/src/components/InfoParagraph.tsx | 48 ++++++ .../components/RichText/RichTextEditor.tsx | 30 ++++ frontend/src/components/Signature.tsx | 20 +++ frontend/src/components/Signatures.tsx | 57 ++++++ .../contracts/ContractTemplatePreview.tsx | 109 ++++++++++++ .../contracts/ContractTemplates.tsx | 80 +++++++++ .../components/contracts/ContractTerms.tsx | 68 ++++++++ .../contracts/ContractTermsContent.tsx | 29 ++++ .../contracts/ContractTermsEmptyState.tsx | 34 ++++ .../contracts/OrganizationDataForm.tsx | 50 ++++++ .../contracts/OrganizationDetails.tsx | 37 ++++ frontend/src/index.css | 17 ++ frontend/src/routes/Router.tsx | 4 + 18 files changed, 849 insertions(+), 18 deletions(-) create mode 100644 frontend/src/assets/images/signature.svg create mode 100644 frontend/src/components/InfoParagraph.tsx create mode 100644 frontend/src/components/RichText/RichTextEditor.tsx create mode 100644 frontend/src/components/Signature.tsx create mode 100644 frontend/src/components/Signatures.tsx create mode 100644 frontend/src/components/contracts/ContractTemplatePreview.tsx create mode 100644 frontend/src/components/contracts/ContractTemplates.tsx create mode 100644 frontend/src/components/contracts/ContractTerms.tsx create mode 100644 frontend/src/components/contracts/ContractTermsContent.tsx create mode 100644 frontend/src/components/contracts/ContractTermsEmptyState.tsx create mode 100644 frontend/src/components/contracts/OrganizationDataForm.tsx create mode 100644 frontend/src/components/contracts/OrganizationDetails.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c60588113..fc53dc702 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,10 +26,12 @@ "react-hook-form": "^7.52.1", "react-i18next": "14.1.2", "react-query": "^3.39.3", + "react-quill": "^2.0.0", "react-router-dom": "6.24.1", "react-select": "^5.8.0", "react-select-async-paginate": "0.7.4", "react-toastify": "^10.0.5", + "react-tooltip": "^5.28.0", "recharts": "^2.13.0-alpha.4", "use-query-params": "^2.2.1", "yup": "^1.4.0" @@ -4507,6 +4509,14 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, + "node_modules/@types/quill": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", + "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", + "dependencies": { + "parchment": "^1.1.2" + } + }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", @@ -5390,7 +5400,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5552,6 +5561,11 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -6064,6 +6078,25 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6094,7 +6127,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -6111,7 +6143,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -6335,7 +6366,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -6347,7 +6377,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -7132,6 +7161,11 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -7152,6 +7186,11 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, "node_modules/fast-equals": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", @@ -7490,7 +7529,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7516,7 +7554,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -7732,7 +7769,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -7780,7 +7816,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -7792,7 +7827,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7804,7 +7838,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7816,7 +7849,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -8160,6 +8192,21 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -8281,7 +8328,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -8418,7 +8464,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -9183,11 +9228,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -9489,6 +9548,11 @@ "tslib": "^2.0.3" } }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9891,6 +9955,45 @@ } ] }, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/quill/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/quill/node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -10053,6 +10156,20 @@ } } }, + "node_modules/react-quill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", + "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==", + "dependencies": { + "@types/quill": "^1.3.10", + "lodash": "^4.17.4", + "quill": "^1.3.7" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -10222,6 +10339,19 @@ "react-dom": ">=18" } }, + "node_modules/react-tooltip": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz", + "integrity": "sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -10337,7 +10467,6 @@ "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, "dependencies": { "call-bind": "^1.0.6", "define-properties": "^1.2.1", @@ -10663,7 +10792,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -10680,7 +10808,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", diff --git a/frontend/package.json b/frontend/package.json index 4535b977c..512480ff9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,10 +29,12 @@ "react-hook-form": "^7.52.1", "react-i18next": "14.1.2", "react-query": "^3.39.3", + "react-quill": "^2.0.0", "react-router-dom": "6.24.1", "react-select": "^5.8.0", "react-select-async-paginate": "0.7.4", "react-toastify": "^10.0.5", + "react-tooltip": "^5.28.0", "recharts": "^2.13.0-alpha.4", "use-query-params": "^2.2.1", "yup": "^1.4.0" diff --git a/frontend/src/assets/images/signature.svg b/frontend/src/assets/images/signature.svg new file mode 100644 index 000000000..57d46283e --- /dev/null +++ b/frontend/src/assets/images/signature.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 9dfe0f15f..3019b1e94 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -971,5 +971,109 @@ "remove_from_list": "Elimină din listă", "confirm": "Confirmă și semnează" } + }, + "doc_templates": { + "title": "Creează template", + "subheading": { + "p1": "Acest template a fost realizat respectând normele din domeniu.", + "p1_link": "Află mai multe", + "p2": "Modelul de contract nu este conform nevoilor organizației tale?", + "p2_link": "Trimite feedback-ul tău" + }, + "table_header": { + "title": "Template contract", + "download_uncompleted": "Descarcă necompletat", + "save": "Salvează" + }, + "tooltip": "Datele contractului (Numărul, perioada contractului și data întemeierii) se completează în momentul atribuirii contractului către unul sau mai mulți voluntari.", + "template_name": "Numele template-ului", + "organization": { + "data": "Datele organizației", + "description": "Datele sunt preluate automat din profilul organizației în NGO Hub", + "view_profile": "Vezi profilul organizației", + "edit": "Editează informațiile organizației pentru acest template de contract, sau treci mai departe.", + "name": "Denumirea oficială a organizației **", + "address": "Sediul social **", + "cui": "CUI/CIF **", + "legal_representative": "Nume prenume reprezentant legal **", + "legal_representative_role": "În calitate de **", + "volunteer_data": { + "title": "Datele voluntarului", + "description": "Datele voluntarului vor fi preluate automat din aplicația VIC, în momentul atribuirii unui contract." + }, + "legal_representative_data": { + "title": "Datele reprezentantului legal", + "description": " Voluntarii sub 16 ani vor trebui sa completeze sectiunea ce conține datele reprezentantului, din aplicația mobilă, în momentul semnării contractului." + } + }, + "volunteer": { + "volunteer": "Voluntar", + "name": "[Nume și prenume voluntar]", + "address": "[Adresă domiciliu voluntar]", + "cnp": "[Număr CNP]", + "series": "[Serie]", + "no": "[Număr]", + "institution": "[Unitate eliberare]", + "eliberation_date": "[Data eliberării]" + }, + "template_preview": { + "title": "CONTRACT DE VOLUNTARIAT", + "p1": { + "no": "Nr.", + "contract_no": "[Numar contract]", + "date": "din data de", + "contract_date": "[Data contract]" + }, + "p2": { + "between": "Între", + "address": "cu sediul în", + "identified": "identificată cu CUI", + "represented_by": "reprezentată prin", + "as": "în calitate de", + "named": "numită în continuare", + "organization": "Organizația" + }, + "and": "și", + "p3": { + "lives": "domiciliat(ă) în", + "cnp": "C.N.P", + "legitimate": "legitimat cu BI/CI seria", + "no": "nr.", + "by": "eliberat de", + "at_date": "la data de", + "named": "numit în continuare", + "volunteer": "Voluntar," + }, + "p4": "s-a convenit încheierea prezentului contract în baza Legii nr. 78/2014 privind reglementarea activității de voluntariat din Romănia." + }, + "contract_duration": { + "title": "DURATA CONTRACTULUI", + "description": "Prezentul contract se încheie pe o perioadă determinată, între data de", + "start": "[Data început]", + "end": "[Data final]" + }, + "contract_terms": { + "title": "Termenii contractului", + "describe": "Descrie termenii contractului", + "add_text": "Adaugă text", + "first_contract": "Primul contract? Citește mai multe despre ce conține un", + "volunteering_contract": "contract de voluntariat", + "cancel": "Anulează", + "save": "Salvează modificări" + }, + "text_editor": { + "placeholder": "Adaugă termenii contractului...", + "title": "Editează text" + }, + "organization_name": "Nume organizație", + "represented_by": "Reprezentată de", + "legal_representative": "Reprezentantul legal al minorului", + "identification": "Serie si nr. CI", + "series": "Serie", + "tel_no": "Nr. Tel.", + "volunteer_name": "Nume voluntar", + "legal_rep_name": "Nume reprezentant legal", + "number": "Număr", + "telephone": "Telefon" } } \ No newline at end of file diff --git a/frontend/src/common/constants/routes.ts b/frontend/src/common/constants/routes.ts index 08592e968..37c9424c4 100644 --- a/frontend/src/common/constants/routes.ts +++ b/frontend/src/common/constants/routes.ts @@ -66,6 +66,11 @@ export const ROUTES: IRoute[] = [ name: i18n.t('general:contracts'), href: 'documents/contracts', }, + { + id: 62, + name: i18n.t('general:templates'), + href: 'documents/templates', + }, ], }, { diff --git a/frontend/src/components/InfoParagraph.tsx b/frontend/src/components/InfoParagraph.tsx new file mode 100644 index 000000000..caed77b4e --- /dev/null +++ b/frontend/src/components/InfoParagraph.tsx @@ -0,0 +1,48 @@ +import React, { DetailedHTMLProps, HTMLAttributes } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Tooltip } from 'react-tooltip'; + +interface InfoParagraphProps + extends DetailedHTMLProps, HTMLParagraphElement> { + text: string; + tooltip?: boolean; + tooltipContent?: string; + highlighted?: boolean; +} + +export const InfoParagraph = ({ + text, + tooltip, + tooltipContent, + highlighted, + className, + ...rest +}: InfoParagraphProps) => { + const { t } = useTranslation('doc_templates'); + + return ( + <> + + {text} + + {tooltip && ( + + )} + + ); +}; diff --git a/frontend/src/components/RichText/RichTextEditor.tsx b/frontend/src/components/RichText/RichTextEditor.tsx new file mode 100644 index 000000000..4aeac4384 --- /dev/null +++ b/frontend/src/components/RichText/RichTextEditor.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import ReactQuill from 'react-quill'; +import 'react-quill/dist/quill.snow.css'; + +const RichTextEditor = ({ + value, + onChange, +}: { + onChange: (value: string) => void; + value: string; +}) => { + const { t } = useTranslation('doc_templates'); + + return ( +
+

{t('text_editor.title')}

+ +
+ ); +}; + +export default RichTextEditor; diff --git a/frontend/src/components/Signature.tsx b/frontend/src/components/Signature.tsx new file mode 100644 index 000000000..6a3862636 --- /dev/null +++ b/frontend/src/components/Signature.tsx @@ -0,0 +1,20 @@ +import React, { ReactNode } from 'react'; +import signature from './../assets/images/signature.svg'; + +interface SignatureProps extends React.HTMLAttributes { + signatureTitle: ReactNode; + p?: ReactNode; +} + +export const Signature = ({ signatureTitle, p, className }: SignatureProps) => { + return ( +
+ {signatureTitle} + {p} + +
+ signature +
+
+ ); +}; diff --git a/frontend/src/components/Signatures.tsx b/frontend/src/components/Signatures.tsx new file mode 100644 index 000000000..a2520d0dc --- /dev/null +++ b/frontend/src/components/Signatures.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { InfoParagraph } from './InfoParagraph'; +import { Signature } from './Signature'; +import { useTranslation } from 'react-i18next'; +import { useOrganizationQuery } from '../services/organization/organization.service'; + +export const Signatures = () => { + const { t } = useTranslation('doc_templates'); + const { data: organization } = useOrganizationQuery(); + + return ( +
+
+ + {t('template_preview.p2.organization')}{' '} + +
+ } + p={ +
+ {t('represented_by')} +
+ } + /> + + {t('volunteer.volunteer')}

} + p={} + /> +
+ +
+ {/* empty div to align items correctly */} +
+ + {t('legal_representative')}

} + p={ +
+ +
+ {t('identification')} ,{' '} + +
+
+ {t('tel_no')} +
+
+ } + className="mt-[-2rem] sm:mt-0" + /> +
+
+ ); +}; diff --git a/frontend/src/components/contracts/ContractTemplatePreview.tsx b/frontend/src/components/contracts/ContractTemplatePreview.tsx new file mode 100644 index 000000000..fab53ae88 --- /dev/null +++ b/frontend/src/components/contracts/ContractTemplatePreview.tsx @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import { InfoParagraph } from '../InfoParagraph'; +import { ContractTerms } from './ContractTerms'; +import { useTranslation } from 'react-i18next'; +import { Signatures } from '../Signatures'; +import { useOrganizationQuery } from '../../services/organization/organization.service'; + +export const ContractTemplatePreview = () => { + const { data: organization } = useOrganizationQuery(); + + const { t } = useTranslation('doc_templates'); + const [infoParagraphHovered, setInfoParagraphHovered] = useState(false); + + const onInfoParagraphHover = () => { + setInfoParagraphHovered(true); + }; + const onInfoParagraphLeave = () => { + setInfoParagraphHovered(false); + }; + + return ( +
+ {/* header */} +

{t('template_preview.title')}

+ + {/* subheader */} +
+

{t('template_preview.p1.no')}

+ +

{t('template_preview.p1.date')}

+ +
+ + {/* P1 */} +

+ {t('template_preview.p2.between')}{' '} + {' '} + {t('template_preview.p2.address')} {' '} + {t('template_preview.p2.identified')}{' '} + {' '} + {t('template_preview.p2.represented_by')}{' '} + {' '} + {t('template_preview.p2.as')}{' '} + {' '} + {t('template_preview.p2.named')}{' '} + {t('template_preview.p2.organization')}{' '} +

+ + {/* P2 */} + +

{t('template_preview.and')}

+ + {/* P3 */} +

+ {t('template_preview.p3.lives')}{' '} + {t('template_preview.p3.cnp')}{' '} + {t('template_preview.p3.legitimate')}{' '} + {t('template_preview.p3.no')}{' '} + {t('template_preview.p3.by')}{' '} + {t('template_preview.p3.at_date')}{' '} + {' '} + {t('template_preview.p3.named')}{' '} + {t('template_preview.p3.volunteer')}{' '} +

+ + {/* P4 */} +

{t('template_preview.p4')}

+ + {/* P5: DURATA CONTRACTULUI */} +

{t('contract_duration.title')}

+

+ {t('contract_duration.description')}{' '} + {' '} + {t('template_preview.and')}{' '} + +

+ + + +
+ ); +}; diff --git a/frontend/src/components/contracts/ContractTemplates.tsx b/frontend/src/components/contracts/ContractTemplates.tsx new file mode 100644 index 000000000..44e824198 --- /dev/null +++ b/frontend/src/components/contracts/ContractTemplates.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import PageLayout from '../../layouts/PageLayout'; +import PageHeader from '../PageHeader'; +import { useNavigate } from 'react-router-dom'; +import CardHeader from '../CardHeader'; +import Card from '../../layouts/CardLayout'; +import CardBody from '../CardBody'; +import Button from '../Button'; +import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; +import { OrganizationDetails } from './OrganizationDetails'; +import { ContractTemplatePreview } from './ContractTemplatePreview'; +import { useTranslation } from 'react-i18next'; + +export const ContractTemplates = () => { + const navigate = useNavigate(); + + const { t } = useTranslation('doc_templates'); + + const navigateBack = () => { + navigate('/documents/contracts', { replace: true }); + }; + + // TODO: translations + // TODO: links for + + return ( + + {t('title')} + + {/* sub header text */} + + + {/* TABLE */} + + {/* TABLE HEADER */} + +
+

{t('table_header.title')}

+
+
+
+
+ + {/* TABLE BODY */} + +
+ + +
+
+
+ + ); + { + /* {isLoading && } */ + } +}; diff --git a/frontend/src/components/contracts/ContractTerms.tsx b/frontend/src/components/contracts/ContractTerms.tsx new file mode 100644 index 000000000..a2991f074 --- /dev/null +++ b/frontend/src/components/contracts/ContractTerms.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { Controller, FieldValues, SubmitHandler, useForm } from 'react-hook-form'; +import Button from '../Button'; +import { ContractTermsEmptyState } from './ContractTermsEmptyState'; +import { useTranslation } from 'react-i18next'; +import RichTextEditor from '../RichText/RichTextEditor'; +import { ContractTermsContent } from './ContractTermsContent'; + +export const ContractTerms = () => { + const { control, handleSubmit, reset } = useForm(); + const { t } = useTranslation('doc_templates'); + const [editingText, setEditingText] = useState(false); + + // todo: initial value for contractTerms taken from the template + const [contractTerms, setContractTerms] = useState(''); + + const onSubmit: SubmitHandler = ({ contractTerms }) => { + // save the new value + setContractTerms(contractTerms); + // close text editor + setEditingText(false); + }; + + // TEXT EDITOR SECTION + if (editingText) { + return ( +
+ void }; + }) => { + return ; + }} + /> +
+
+
+ ); + } + + if (!contractTerms) { + return ; + } + // normal text + return ; +}; diff --git a/frontend/src/components/contracts/ContractTermsContent.tsx b/frontend/src/components/contracts/ContractTermsContent.tsx new file mode 100644 index 000000000..154b76e0f --- /dev/null +++ b/frontend/src/components/contracts/ContractTermsContent.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import Button from '../Button'; +import { PencilIcon } from '@heroicons/react/24/solid'; +import { useTranslation } from 'react-i18next'; + +export const ContractTermsContent = ({ + innerContent, + setEditingText, +}: { + innerContent: string; + setEditingText: React.Dispatch>; +}) => { + const { t } = useTranslation('doc_templates'); + + return ( +
+
+

{t('contract_terms.title')}

+
+
+
+ ); +}; diff --git a/frontend/src/components/contracts/ContractTermsEmptyState.tsx b/frontend/src/components/contracts/ContractTermsEmptyState.tsx new file mode 100644 index 000000000..f27003dd6 --- /dev/null +++ b/frontend/src/components/contracts/ContractTermsEmptyState.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import Button from '../Button'; +import { PencilIcon } from '@heroicons/react/24/solid'; +import { useTranslation } from 'react-i18next'; + +export const ContractTermsEmptyState = ({ + setEditingText, +}: { + setEditingText: React.Dispatch>; +}) => { + const { t } = useTranslation('doc_templates'); + + return ( +
+

{t('contract_terms.title')}

+ +
+

{t('contract_terms.describe')}

+
+
+ ); +}; diff --git a/frontend/src/components/contracts/OrganizationDataForm.tsx b/frontend/src/components/contracts/OrganizationDataForm.tsx new file mode 100644 index 000000000..1bfaf54d0 --- /dev/null +++ b/frontend/src/components/contracts/OrganizationDataForm.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import FormInput from '../FormInput'; +import { useTranslation } from 'react-i18next'; +import { useOrganizationQuery } from '../../services/organization/organization.service'; + +export const OrganizationDataForm = () => { + const { t } = useTranslation('doc_templates'); + + const { data: organization } = useOrganizationQuery(); + + return ( + <> + + + + + + + ); +}; diff --git a/frontend/src/components/contracts/OrganizationDetails.tsx b/frontend/src/components/contracts/OrganizationDetails.tsx new file mode 100644 index 000000000..8ac83bfc5 --- /dev/null +++ b/frontend/src/components/contracts/OrganizationDetails.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import FormInput from '../FormInput'; +import { useTranslation } from 'react-i18next'; +import { OrganizationDataForm } from './OrganizationDataForm'; + +export const OrganizationDetails = () => { + const { t } = useTranslation('doc_templates'); + + return ( +
+ {/* //TODO: functionality to save the contract template name */} + {/* //? DO WE KEEP THIS INPUT HERE OR MOVE IT SOMEWHERE ELSE */} + + +

{t('organization.data')}

+

+ {t('organization.description')} ({/* //TODO: href */} + + {t('organization.view_profile')} + + ). {t('organization.edit')} +

+ + + +
+

{t('organization.volunteer_data.title')}

+

{t('organization.volunteer_data.description')}

+
+ +
+

{t('organization.legal_representative_data.title')}

+

{t('organization.legal_representative_data.description')}

+
+
+ ); +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index 12429ff4b..43bfca06c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -16,6 +16,23 @@ @apply font-roboto text-sm sm:text-base; } + ul, + ol { + @apply pl-5 my-2; + } + + ul { + @apply list-disc; + } + + ol { + @apply list-decimal; + } + + li { + @apply mb-1; + } + header { @apply shadow-header; } diff --git a/frontend/src/routes/Router.tsx b/frontend/src/routes/Router.tsx index 2932c2408..8d5db3f20 100644 --- a/frontend/src/routes/Router.tsx +++ b/frontend/src/routes/Router.tsx @@ -35,6 +35,7 @@ import AddContractTemplate from '../pages/AddContractTemplate'; import EditContractTemplate from '../pages/EditContractTemplate'; import AddContract from '../containers/query/AddContractWithQueryParams'; import ActionsArchive from '../pages/ActionsArchive'; +import { ContractTemplates } from '../components/contracts/ContractTemplates'; const Router = () => { return ( @@ -92,6 +93,9 @@ const Router = () => { } /> } /> + }> + } /> + From f21781231d5cae88a6b03f4c4e61a70b1c5087c4 Mon Sep 17 00:00:00 2001 From: luciatugui Date: Fri, 30 Aug 2024 19:22:26 +0300 Subject: [PATCH 05/55] fix: comma in paragraphs --- .../contracts/ContractTemplatePreview.tsx | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/contracts/ContractTemplatePreview.tsx b/frontend/src/components/contracts/ContractTemplatePreview.tsx index fab53ae88..24ca47863 100644 --- a/frontend/src/components/contracts/ContractTemplatePreview.tsx +++ b/frontend/src/components/contracts/ContractTemplatePreview.tsx @@ -46,15 +46,19 @@ export const ContractTemplatePreview = () => { {/* P1 */}

{t('template_preview.p2.between')}{' '} - {' '} - {t('template_preview.p2.address')} {' '} + + {', '} + {t('template_preview.p2.address')} + {', '} {t('template_preview.p2.identified')}{' '} - {' '} + + {', '} {t('template_preview.p2.represented_by')}{' '} {' '} + /> + {', '} {t('template_preview.p2.as')}{' '} {' '} {t('template_preview.p2.named')}{' '} @@ -67,13 +71,14 @@ export const ContractTemplatePreview = () => { {/* P3 */}

- {t('template_preview.p3.lives')}{' '} - {t('template_preview.p3.cnp')}{' '} - {t('template_preview.p3.legitimate')}{' '} + , {t('template_preview.p3.lives')}{' '} + , {t('template_preview.p3.cnp')}{' '} + , {t('template_preview.p3.legitimate')}{' '} {t('template_preview.p3.no')}{' '} - {t('template_preview.p3.by')}{' '} - {t('template_preview.p3.at_date')}{' '} - {' '} + , {t('template_preview.p3.by')}{' '} + , {t('template_preview.p3.at_date')}{' '} + + {', '} {t('template_preview.p3.named')}{' '} {t('template_preview.p3.volunteer')}{' '}

From 5f04153052ad4f1289509500b1dd42be3755df49 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Mon, 2 Sep 2024 11:47:23 +0300 Subject: [PATCH 06/55] feat: [contracts] - working version of lambda pdf generator --- pdf-generator-puppeteer/.gitignore | 2 + pdf-generator-puppeteer/README.md | 68 ++ pdf-generator-puppeteer/handler.js | 51 ++ pdf-generator-puppeteer/package-lock.json | 971 ++++++++++++++++++++++ pdf-generator-puppeteer/package.json | 16 + pdf-generator-puppeteer/serverless.yml | 21 + 6 files changed, 1129 insertions(+) create mode 100644 pdf-generator-puppeteer/.gitignore create mode 100644 pdf-generator-puppeteer/README.md create mode 100644 pdf-generator-puppeteer/handler.js create mode 100644 pdf-generator-puppeteer/package-lock.json create mode 100644 pdf-generator-puppeteer/package.json create mode 100644 pdf-generator-puppeteer/serverless.yml diff --git a/pdf-generator-puppeteer/.gitignore b/pdf-generator-puppeteer/.gitignore new file mode 100644 index 000000000..d19fb7df5 --- /dev/null +++ b/pdf-generator-puppeteer/.gitignore @@ -0,0 +1,2 @@ +node_modules +.serverless \ No newline at end of file diff --git a/pdf-generator-puppeteer/README.md b/pdf-generator-puppeteer/README.md new file mode 100644 index 000000000..3c7c034a4 --- /dev/null +++ b/pdf-generator-puppeteer/README.md @@ -0,0 +1,68 @@ + + +# Serverless Framework AWS NodeJS Example + +This template demonstrates how to deploy a simple NodeJS function running on AWS Lambda using the Serverless Framework. The deployed function does not include any event definitions or any kind of persistence (database). For more advanced configurations check out the [examples repo](https://github.com/serverless/examples/) which include use cases like API endpoints, workers triggered by SQS, persistence with DynamoDB, and scheduled tasks. For details about configuration of specific events, please refer to our [documentation](https://www.serverless.com/framework/docs/providers/aws/events/). + +## Usage + +### Deployment + +In order to deploy the example, you need to run the following command: + +``` +serverless deploy +``` + +After running deploy, you should see output similar to: + +``` +Deploying "aws-node" to stage "dev" (us-east-1) + +✔ Service deployed to stack aws-node-dev (90s) + +functions: + hello: aws-node-dev-hello (1.5 kB) +``` + +### Invocation + +After successful deployment, you can invoke the deployed function by using the following command: + +``` +serverless invoke --function hello +``` + +Which should result in response similar to the following: + +```json +{ + "statusCode": 200, + "body": "{\"message\":\"Go Serverless v4.0! Your function executed successfully!\"}" +} +``` + +### Local development + +The easiest way to develop and test your function is to use the Serverless Framework's `dev` command: + +``` +serverless dev +``` + +This will start a local emulator of AWS Lambda and tunnel your requests to and from AWS Lambda, allowing you to interact with your function as if it were running in the cloud. + +Now you can invoke the function as before, but this time the function will be executed locally. Now you can develop your function locally, invoke it, and see the results immediately without having to re-deploy. + +When you are done developing, don't forget to run `serverless deploy` to deploy the function to the cloud. diff --git a/pdf-generator-puppeteer/handler.js b/pdf-generator-puppeteer/handler.js new file mode 100644 index 000000000..bcbc818bc --- /dev/null +++ b/pdf-generator-puppeteer/handler.js @@ -0,0 +1,51 @@ +const puppeteer = require("puppeteer-core"); +const chromium = require("@sparticuz/chromium"); +const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); + +const s3 = new S3Client({ region: 'us-east-1' }); + +chromium.setHeadlessMode = true; + +exports.hello = async (event) => { + + const browser = await puppeteer.launch({ + executablePath: await chromium.executablePath(), + headless: chromium.headless, + ignoreHTTPSErrors: true, + // defaultViewport: chromium.defaultViewport, + args: [...chromium.args, "--hide-scrollbars", "--disable-web-security", '--no-sandbox', '--disable-setuid-sandbox'], + }); + + const page = await browser.newPage(); + await page.setContent(event.body); + const buffer = await page.pdf({ format: 'A4' }); + await browser.close(); + + const s3Params = { + Bucket: 'test-pdfs-vic', // replace with your bucket name + Key: `document-${+new Date()}.pdf`, // replace with your desired file name + Body: buffer, + ContentType: 'application/pdf', + }; + + + try { + const command = new PutObjectCommand(s3Params); + const fileInS3 = await s3.send(command); + } catch (error) { + console.error('Error uploading PDF to S3:', error); + throw new Error('Failed to upload PDF to S3'); + } + + console.log({ url: `https://${s3Params.Bucket}.s3.amazonaws.com/${s3Params.Key}` }); + + return { + statusCode: 200, + body: JSON.stringify({ + message: 'PDF successfully uploaded to S3', + url: `https://${s3Params.Bucket}.s3.amazonaws.com/${s3Params.Key}`, + }), + }; +}; + + diff --git a/pdf-generator-puppeteer/package-lock.json b/pdf-generator-puppeteer/package-lock.json new file mode 100644 index 000000000..6d3e72e10 --- /dev/null +++ b/pdf-generator-puppeteer/package-lock.json @@ -0,0 +1,971 @@ +{ + "name": "pdf-generator-puppeteer", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pdf-generator-puppeteer", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@sparticuz/chromium": "^123.0.1", + "puppeteer-core": "^22.6.4" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.1.tgz", + "integrity": "sha512-QSXujx4d4ogDamQA8ckkkRieFzDgZEuZuGiey9G7CuDcbnX4iINKWxTPC5Br2AEzY9ICAvcndqgAUFMMKnS/Tw==", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.4.0", + "semver": "7.6.0", + "tar-fs": "3.0.5", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sparticuz/chromium": { + "version": "123.0.1", + "resolved": "https://registry.npmjs.org/@sparticuz/chromium/-/chromium-123.0.1.tgz", + "integrity": "sha512-RPrA99xrddbXXZjhUH1WZkTCK3QDQ77t/0y+vULtiW1uAlWLBxorxGnNTuzBDk6eNolxOVFrXZ2aoAN+/eB5TA==", + "dependencies": { + "follow-redirects": "^1.15.6", + "tar-fs": "^3.0.5" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/@types/node": { + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", + "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", + "optional": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.2.tgz", + "integrity": "sha512-Kcq/FG3lhspzGHK+Q0IMfImuFOmaW/jFofBAUJuuG7H67879JeaPUppUHhgLjJKenfxiO6Ix2AGSd47Pf7mRxg==", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.1.tgz", + "integrity": "sha512-yQC/blMP/eUdULsF7hrcC9tUFXlUmAWRbSQndEln77nOIh/N4Loaqch/MA4hyoDKhw1Zd1Wj+uLV/bT6lC/4BQ==", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.2.0.tgz", + "integrity": "sha512-+o9MG5bPRRBlkVSpfFlMag3n7wMaIZb4YZasU2+/96f+3HTQ4F9DKQeu3K/Sjz1W0umu6xvVq1ON0ipWdMlr3A==", + "optional": true, + "dependencies": { + "streamx": "^2.18.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/chromium-bidi": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.17.tgz", + "integrity": "sha512-BqOuIWUgTPj8ayuBFJUYCCuwIcwjBsb3/614P7tt1bEPJ4i1M0kCdIl0Wi9xhtswBXnfO2bTpTMkHD71H8rJMg==", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.22.4" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1262051", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1262051.tgz", + "integrity": "sha512-YJe4CT5SA8on3Spa+UDtNhEqtuV6Epwz3OZ4HQVLhlRccpZ9/PAYk0/cy/oKxFKRrZPBUPyxympQci4yWNWZ9g==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer-core": { + "version": "22.6.4", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.6.4.tgz", + "integrity": "sha512-QtfJwPmqQec3EHc6LqbEz03vSiuVAr9bYp0TV87dLoreev6ZevsXdLgOfQgoA3GocrsSe/eUf7NRPQ1lQfsc3w==", + "dependencies": { + "@puppeteer/browsers": "2.2.1", + "chromium-bidi": "0.5.17", + "debug": "4.3.4", + "devtools-protocol": "0.0.1262051", + "ws": "8.16.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/streamx": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.19.0.tgz", + "integrity": "sha512-5z6CNR4gtkPbwlxyEqoDGDmWIzoNJqCBt4Eac1ICP9YaIT08ct712cFj0u1rx4F8luAuL+3Qc+RFIdI4OX00kg==", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/pdf-generator-puppeteer/package.json b/pdf-generator-puppeteer/package.json new file mode 100644 index 000000000..dd644250d --- /dev/null +++ b/pdf-generator-puppeteer/package.json @@ -0,0 +1,16 @@ +{ + "name": "pdf-generator-puppeteer", + "version": "1.0.0", + "description": "", + "main": "handler.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@sparticuz/chromium": "^123.0.1", + "puppeteer-core": "^22.6.4" + } +} diff --git a/pdf-generator-puppeteer/serverless.yml b/pdf-generator-puppeteer/serverless.yml new file mode 100644 index 000000000..0f75f0eb9 --- /dev/null +++ b/pdf-generator-puppeteer/serverless.yml @@ -0,0 +1,21 @@ +# "org" ensures this Service is used with the correct Serverless Framework Access Key. +# org: tribus +# "app" enables Serverless Framework Dashboard features and sharing them with other Services. +app: pdf-generator +# "service" is the name of this project. This will also be added to your AWS resource names. +service: pdf-generator-puppeteer + +# package: +# individually: true +# patterns: +# - '!node_modules/@sparticuz/chromium/bin/**' + +provider: + name: aws + runtime: nodejs20.x + +functions: + hello: + handler: handler.hello + events: + - httpApi: 'POST /test' From c35e38e10726c35758141cc8a174846482fa4f6d Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Mon, 2 Sep 2024 11:48:06 +0300 Subject: [PATCH 07/55] feat: [contracts] - change public/pdf endpoint to call the lambda PDF generator --- backend/src/api/public/public.controller.ts | 19 +++---------------- backend/src/common/helpers/pdf-from-html.ts | 1 + .../documents/services/pdf-generator.ts | 12 +++++++++--- .../documents/generate-pdfs.usecase.ts | 2 +- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/backend/src/api/public/public.controller.ts b/backend/src/api/public/public.controller.ts index d898e86e0..c8e34a778 100644 --- a/backend/src/api/public/public.controller.ts +++ b/backend/src/api/public/public.controller.ts @@ -1,7 +1,6 @@ -import { Controller, Get, Res } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { SkipThrottle } from '@nestjs/throttler'; import { APP_VERSION } from 'src/common/constants/version'; -import { Response } from 'express'; import { GeneratePDFsUseCase } from 'src/usecases/documents/generate-pdfs.usecase'; @Controller('public') @@ -21,19 +20,7 @@ export class PublicController { } @Get('pdf') - async pdf(@Res() res: Response): Promise { - const buffer = await this.generatePDFsUseCase.execute(); - res.set({ - // pdf - 'Content-Type': 'application/pdf', - // 'Content-Disposition': 'attachment; filename=invoice.pdf', - 'Content-Disposition': 'inline; filename=invoice.pdf', - 'Content-Length': buffer.length, - // prevent cache - 'Cache-Control': 'no-cache, no-store, must-revalidate', - Pragma: 'no-cache', - Expires: 0, - }); - res.end(buffer); + async pdf(): Promise { + return this.generatePDFsUseCase.execute(); } } diff --git a/backend/src/common/helpers/pdf-from-html.ts b/backend/src/common/helpers/pdf-from-html.ts index 0d56de21b..d8013bad6 100644 --- a/backend/src/common/helpers/pdf-from-html.ts +++ b/backend/src/common/helpers/pdf-from-html.ts @@ -1,6 +1,7 @@ import puppeteer from 'puppeteer'; export const HTMLtoPDF = async (html: string): Promise => { + // TODO: https://www.flightcontrol.dev/docs/tips/deployment/puppeteer const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], diff --git a/backend/src/modules/documents/services/pdf-generator.ts b/backend/src/modules/documents/services/pdf-generator.ts index 7f425c25e..9987b5214 100644 --- a/backend/src/modules/documents/services/pdf-generator.ts +++ b/backend/src/modules/documents/services/pdf-generator.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { resolve } from 'path'; import * as fs from 'fs'; -import { HTMLtoPDF } from 'src/common/helpers/pdf-from-html'; import Handlebars from 'handlebars'; +import axios from 'axios'; @Injectable() export class PDFGenerator { @@ -19,7 +19,7 @@ export class PDFGenerator { Handlebars.registerPartial('header', template); } - public async generatePDF(): Promise { + public async generatePDF(): Promise { const templateDir = resolve(__dirname, '..', 'templates', 'contract.hbs'); const file = fs.readFileSync(templateDir, 'utf-8'); const template = Handlebars.compile(file); @@ -28,6 +28,12 @@ export class PDFGenerator { subtitle: `
Text
`, }); - return HTMLtoPDF(fileHTML); + // return HTMLtoPDF(fileHTML); + return axios + .post( + 'https://iywe2rp7u1.execute-api.us-east-1.amazonaws.com/test', + fileHTML, + ) + .then((res) => res.data); } } diff --git a/backend/src/usecases/documents/generate-pdfs.usecase.ts b/backend/src/usecases/documents/generate-pdfs.usecase.ts index 2fbafb87f..e01421cd4 100644 --- a/backend/src/usecases/documents/generate-pdfs.usecase.ts +++ b/backend/src/usecases/documents/generate-pdfs.usecase.ts @@ -6,7 +6,7 @@ import { PDFGenerator } from 'src/modules/documents/services/pdf-generator'; export class GeneratePDFsUseCase implements IUseCaseService { constructor(private readonly pDFGenerator: PDFGenerator) {} - public async execute(): Promise { + public async execute(): Promise { return this.pDFGenerator.generatePDF(); } } From 7e388560b3930a022da3bdd158e213570d0fba91 Mon Sep 17 00:00:00 2001 From: luciatugui Date: Mon, 2 Sep 2024 16:18:10 +0300 Subject: [PATCH 08/55] wip: translations, error and loading states for organization data --- .../src/assets/locales/en/translation.json | 108 ++++++++++++++++++ .../src/assets/locales/ro/translation.json | 4 + .../src/components/OrganizationDataError.tsx | 22 ++++ .../contracts/ContractTemplatePreview.tsx | 30 ++++- .../contracts/OrganizationDataForm.tsx | 11 +- .../contracts/OrganizationDetails.tsx | 12 +- .../contracts => pages}/ContractTemplates.tsx | 17 ++- 7 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/OrganizationDataError.tsx rename frontend/src/{components/contracts => pages}/ContractTemplates.tsx (83%) diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json index 679c4ebcb..337fa258f 100644 --- a/frontend/src/assets/locales/en/translation.json +++ b/frontend/src/assets/locales/en/translation.json @@ -971,5 +971,113 @@ "remove_from_list": "Remove from list", "confirm": "Confirm and sign" } + }, + "doc_templates": { + "title": "Create template", + "subheading": { + "p1": "This template was created respecting the rules in the field.", + "p1_link": "Learn more", + "p2": "The contract model is not conform to the organization's needs?", + "p2_link": "Send your feedback" + }, + "table_header": { + "title": "Contract template", + "download_uncompleted": "Download uncompleted", + "save": "Save" + }, + "tooltip": "The contract data (Number, contract period, and establishment date) is automatically completed when the contract is assigned to one or more volunteers.", + "template_name": "Template name", + "organization": { + "data": "Organization data", + "description": "The data is automatically retrieved from the organization's profile in NGO Hub", + "view_profile": "View organization profile", + "edit": "Edit the organization's information for this contract template, or move on.", + "name": "Official organization name **", + "address": "Headquarters address **", + "cui": "CUI/CIF **", + "legal_representative": "Legal representative name **", + "legal_representative_role": "Role **", + "volunteer_data": { + "title": "Volunteer data", + "description": "The volunteer's data will be automatically retrieved from the VIC application when a contract is assigned." + }, + "legal_representative_data": { + "title": "Legal representative's data", + "description": "Volunteers under the age of 16 will need to complete the section containing the legal representative's data in the mobile application when signing the contract." + }, + "organization_data_form": { + "loading_error": "We encountered a problem while loading the organization data", + "retry_button": "Retry" + } + }, + "volunteer": { + "volunteer": "Volunteer", + "name": "[Volunteer full name]", + "address": "[Volunteer home address]", + "cnp": "[CNP number]", + "series": "[Series]", + "no": "[Number]", + "institution": "[Issuing institution]", + "eliberation_date": "[Issuance date]" + }, + "template_preview": { + "title": "VOLUNTEERING CONTRACT", + "p1": { + "no": "No.", + "contract_no": "[Contract number]", + "date": "dated", + "contract_date": "[Contract date]" + }, + "p2": { + "between": "Between", + "address": "headquartered at", + "identified": "identified with CUI", + "represented_by": "represented by", + "as": "as", + "named": "hereinafter referred to as", + "organization": "Organization" + }, + "and": "and", + "p3": { + "lives": "residing at", + "cnp": "C.N.P", + "legitimate": "legitimated with ID series", + "no": "no.", + "by": "issued by", + "at_date": "on", + "named": "hereinafter referred to as", + "volunteer": "Volunteer," + }, + "p4": "it was agreed to conclude this contract under Law no. 78/2014 regarding the regulation of volunteering activity in Romania." + }, + "contract_duration": { + "title": "CONTRACT DURATION", + "description": "This contract is concluded for a fixed period, between", + "start": "[Start date]", + "end": "[End date]" + }, + "contract_terms": { + "title": "Contract terms", + "describe": "Describe the contract terms", + "add_text": "Add text", + "first_contract": "First contract? Read more about what's included in a", + "volunteering_contract": "volunteering contract", + "cancel": "Cancel", + "save": "Save changes" + }, + "text_editor": { + "placeholder": "Add contract terms...", + "title": "Edit text" + }, + "organization_name": "Organization name", + "represented_by": "Represented by", + "legal_representative": "Legal representative of the minor", + "identification": "ID series and number", + "series": "Series", + "tel_no": "Phone number", + "volunteer_name": "Volunteer name", + "legal_rep_name": "Legal representative's name", + "number": "Number", + "telephone": "Phone" } } \ No newline at end of file diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 3019b1e94..d2c06ac05 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -1004,6 +1004,10 @@ "legal_representative_data": { "title": "Datele reprezentantului legal", "description": " Voluntarii sub 16 ani vor trebui sa completeze sectiunea ce conține datele reprezentantului, din aplicația mobilă, în momentul semnării contractului." + }, + "organization_data_form": { + "loading_error": "Am întâmpinat o problemă la încărcarea datelor despre organizație", + "retry_button": "Reîncearcă" } }, "volunteer": { diff --git a/frontend/src/components/OrganizationDataError.tsx b/frontend/src/components/OrganizationDataError.tsx new file mode 100644 index 000000000..45f613fef --- /dev/null +++ b/frontend/src/components/OrganizationDataError.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { ExclamationCircleIcon } from '@heroicons/react/24/outline'; +import Button from './Button'; +import { useTranslation } from 'react-i18next'; + +export const OrganizationDataError = ({ onRetry }: { onRetry: () => void }) => { + const { t } = useTranslation('doc_templates'); + + return ( +
+ +

+ {t('organization.organization_data_form.loading_error')} +

+
+ ); +}; diff --git a/frontend/src/components/contracts/ContractTemplatePreview.tsx b/frontend/src/components/contracts/ContractTemplatePreview.tsx index 24ca47863..8ce068c71 100644 --- a/frontend/src/components/contracts/ContractTemplatePreview.tsx +++ b/frontend/src/components/contracts/ContractTemplatePreview.tsx @@ -4,13 +4,24 @@ import { ContractTerms } from './ContractTerms'; import { useTranslation } from 'react-i18next'; import { Signatures } from '../Signatures'; import { useOrganizationQuery } from '../../services/organization/organization.service'; +import LoadingContent from '../LoadingContent'; +import { OrganizationDataError } from '../OrganizationDataError'; export const ContractTemplatePreview = () => { - const { data: organization } = useOrganizationQuery(); - const { t } = useTranslation('doc_templates'); const [infoParagraphHovered, setInfoParagraphHovered] = useState(false); + const { + data: organization, + isLoading: isLoadingOrganizationData, + isError: isErrorOrganizationData, + refetch: refetchOrganizationData, + } = useOrganizationQuery(); + + const handleRefetch = () => { + refetchOrganizationData(); + }; + const onInfoParagraphHover = () => { setInfoParagraphHovered(true); }; @@ -18,6 +29,21 @@ export const ContractTemplatePreview = () => { setInfoParagraphHovered(false); }; + if (isLoadingOrganizationData) { + return ( +
+ +
+ ); + } + + if (isErrorOrganizationData) + return ( +
+ +
+ ); + return (
{/* header */} diff --git a/frontend/src/components/contracts/OrganizationDataForm.tsx b/frontend/src/components/contracts/OrganizationDataForm.tsx index 1bfaf54d0..904659bf9 100644 --- a/frontend/src/components/contracts/OrganizationDataForm.tsx +++ b/frontend/src/components/contracts/OrganizationDataForm.tsx @@ -2,11 +2,20 @@ import React from 'react'; import FormInput from '../FormInput'; import { useTranslation } from 'react-i18next'; import { useOrganizationQuery } from '../../services/organization/organization.service'; +import LoadingContent from '../LoadingContent'; +import { OrganizationDataError } from '../OrganizationDataError'; export const OrganizationDataForm = () => { const { t } = useTranslation('doc_templates'); - const { data: organization } = useOrganizationQuery(); + const { data: organization, isLoading, isError, refetch } = useOrganizationQuery(); + + const handleRefetch = () => { + refetch(); + }; + + if (isLoading) return ; + if (isError) return ; return ( <> diff --git a/frontend/src/components/contracts/OrganizationDetails.tsx b/frontend/src/components/contracts/OrganizationDetails.tsx index 8ac83bfc5..06f551530 100644 --- a/frontend/src/components/contracts/OrganizationDetails.tsx +++ b/frontend/src/components/contracts/OrganizationDetails.tsx @@ -2,15 +2,23 @@ import React from 'react'; import FormInput from '../FormInput'; import { useTranslation } from 'react-i18next'; import { OrganizationDataForm } from './OrganizationDataForm'; +import { Controller, useForm } from 'react-hook-form'; export const OrganizationDetails = () => { const { t } = useTranslation('doc_templates'); + const { control } = useForm(); return (
{/* //TODO: functionality to save the contract template name */} - {/* //? DO WE KEEP THIS INPUT HERE OR MOVE IT SOMEWHERE ELSE */} - + ( + + )} + />

{t('organization.data')}

diff --git a/frontend/src/components/contracts/ContractTemplates.tsx b/frontend/src/pages/ContractTemplates.tsx similarity index 83% rename from frontend/src/components/contracts/ContractTemplates.tsx rename to frontend/src/pages/ContractTemplates.tsx index 44e824198..8ea1b71a8 100644 --- a/frontend/src/components/contracts/ContractTemplates.tsx +++ b/frontend/src/pages/ContractTemplates.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import PageLayout from '../../layouts/PageLayout'; -import PageHeader from '../PageHeader'; +import PageLayout from '../layouts/PageLayout'; +import PageHeader from '../components/PageHeader'; import { useNavigate } from 'react-router-dom'; -import CardHeader from '../CardHeader'; -import Card from '../../layouts/CardLayout'; -import CardBody from '../CardBody'; -import Button from '../Button'; +import CardHeader from '../components/CardHeader'; +import Card from '../layouts/CardLayout'; +import CardBody from '../components/CardBody'; +import Button from '../components/Button'; import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; -import { OrganizationDetails } from './OrganizationDetails'; -import { ContractTemplatePreview } from './ContractTemplatePreview'; +import { OrganizationDetails } from '../components/contracts/OrganizationDetails'; +import { ContractTemplatePreview } from '../components/contracts/ContractTemplatePreview'; import { useTranslation } from 'react-i18next'; export const ContractTemplates = () => { @@ -20,7 +20,6 @@ export const ContractTemplates = () => { navigate('/documents/contracts', { replace: true }); }; - // TODO: translations // TODO: links for return ( From ff94be9d04587ff3642b4b46a8d3f28ac54ef82d Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Tue, 3 Sep 2024 11:57:45 +0300 Subject: [PATCH 09/55] feat: [contracts] Fetch more data from ONGHub to accomodate template generation --- .../organization/organization.controller.ts | 42 +++++++++-- .../organization-presenter.interface.ts | 21 ++++++ .../1725349961532-AddOrganizationNewFields.ts | 27 ++++++++ .../modules/onghub/exceptions/exceptions.ts | 5 ++ .../entities/organization.entity.ts | 13 ++++ .../organization-repository.interface.ts | 6 +- .../organization/models/organization.model.ts | 13 ++++ .../repositories/organization.repository.ts | 5 +- .../services/organization.facade.ts | 10 ++- .../organization/sync-with-ngohub.usecase.ts | 69 +++++++++++++++++++ backend/src/usecases/use-case.module.ts | 3 + .../src/assets/locales/en/translation.json | 5 +- .../src/assets/locales/ro/translation.json | 5 +- .../interfaces/organization.interface.ts | 3 + frontend/src/pages/Organization.tsx | 52 ++++++++++++-- .../services/organization/organization.api.ts | 3 + .../organization/organization.service.ts | 7 ++ 17 files changed, 269 insertions(+), 20 deletions(-) create mode 100644 backend/src/migrations/1725349961532-AddOrganizationNewFields.ts create mode 100644 backend/src/usecases/organization/sync-with-ngohub.usecase.ts diff --git a/backend/src/api/organization/organization.controller.ts b/backend/src/api/organization/organization.controller.ts index b69db9cc8..c0e0d0180 100644 --- a/backend/src/api/organization/organization.controller.ts +++ b/backend/src/api/organization/organization.controller.ts @@ -1,5 +1,14 @@ -import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { ApiBearerAuth, ApiBody } from '@nestjs/swagger'; +import { Request } from 'express'; import { ExtractUser } from 'src/common/decorators/extract-user.decorator'; import { WebJwtAuthGuard } from 'src/modules/auth/guards/jwt-web.guard'; import { IAdminUserModel } from 'src/modules/user/models/admin-user.model'; @@ -7,6 +16,7 @@ import { GetOrganizationUseCaseService } from 'src/usecases/organization/get-org import { UpdateOrganizationDescriptionUseCaseService } from 'src/usecases/organization/update-organization-description.usecase'; import { UpdateOrganizationDescriptionDto } from './dto/update-organization-description.dto'; import { OrganizationPresenter } from './presenters/organization-presenter.interface'; +import { SyncWithOngHubUseCaseService } from 'src/usecases/organization/sync-with-ngohub.usecase'; // @Roles(Role.ADMIN) @ApiBearerAuth() @@ -16,24 +26,42 @@ export class OrganizationController { constructor( private readonly getOrganizationUseCase: GetOrganizationUseCaseService, private readonly updateOrganizationDescriptionUseCase: UpdateOrganizationDescriptionUseCaseService, + private readonly syncWithOngHubUseCase: SyncWithOngHubUseCaseService, ) {} @Get() - getOrganization( + async getOrganization( @ExtractUser() { organizationId }: IAdminUserModel, ): Promise { - return this.getOrganizationUseCase.execute(organizationId); + const organization = + await this.getOrganizationUseCase.execute(organizationId); + return new OrganizationPresenter(organization); } @ApiBody({ type: UpdateOrganizationDescriptionDto }) @Patch() - patchOrganization( + async patchOrganization( @ExtractUser() admin: IAdminUserModel, @Body() { description }: UpdateOrganizationDescriptionDto, ): Promise { - return this.updateOrganizationDescriptionUseCase.execute( - description, - admin, + const organization = + await this.updateOrganizationDescriptionUseCase.execute( + description, + admin, + ); + return new OrganizationPresenter(organization); + } + + @Post('onghub/sync') + async resyncWithOngHub( + @ExtractUser() { organizationId }: IAdminUserModel, + @Req() req: Request, + ): Promise { + const organization = await this.syncWithOngHubUseCase.execute( + organizationId, + req.headers.authorization.split(' ')[1], ); + + return new OrganizationPresenter(organization); } } diff --git a/backend/src/api/organization/presenters/organization-presenter.interface.ts b/backend/src/api/organization/presenters/organization-presenter.interface.ts index 01d6ece80..69ec3acaf 100644 --- a/backend/src/api/organization/presenters/organization-presenter.interface.ts +++ b/backend/src/api/organization/presenters/organization-presenter.interface.ts @@ -12,6 +12,9 @@ export class OrganizationPresenter { this.activityArea = organization.activityArea; this.logo = organization.logo; this.description = organization.description; + this.cui = organization.cui; + this.legalReprezentativeFullName = organization.legalReprezentativeFullName; + this.legalReprezentativeRole = organization.legalReprezentativeRole; } @Expose() @@ -63,4 +66,22 @@ export class OrganizationPresenter { description: 'The description phone of the Organization', }) description: string; + + @Expose() + @ApiProperty({ + description: 'CUI Organization', + }) + cui: string; + + @Expose() + @ApiProperty({ + description: 'The legal representative full name of the Organization', + }) + legalReprezentativeFullName: string; + + @Expose() + @ApiProperty({ + description: 'The legal representative role of the Organization', + }) + legalReprezentativeRole: string; } diff --git a/backend/src/migrations/1725349961532-AddOrganizationNewFields.ts b/backend/src/migrations/1725349961532-AddOrganizationNewFields.ts new file mode 100644 index 000000000..d678f36f7 --- /dev/null +++ b/backend/src/migrations/1725349961532-AddOrganizationNewFields.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddOrganizationNewFields1725349961532 + implements MigrationInterface +{ + name = 'AddOrganizationNewFields1725349961532'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "organization" ADD "cui" text`); + await queryRunner.query( + `ALTER TABLE "organization" ADD "legal_representative_full_name" text`, + ); + await queryRunner.query( + `ALTER TABLE "organization" ADD "legal_representative_role" text`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "organization" DROP COLUMN "legal_representative_role"`, + ); + await queryRunner.query( + `ALTER TABLE "organization" DROP COLUMN "legal_representative_full_name"`, + ); + await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "cui"`); + } +} diff --git a/backend/src/modules/onghub/exceptions/exceptions.ts b/backend/src/modules/onghub/exceptions/exceptions.ts index fdaa82cec..d2da1c4f0 100644 --- a/backend/src/modules/onghub/exceptions/exceptions.ts +++ b/backend/src/modules/onghub/exceptions/exceptions.ts @@ -4,6 +4,7 @@ export enum OngHubExceptionCodes { ONG_001 = 'ONG_001', ONG_002 = 'ONG_002', ONG_003 = 'ONG_003', + ONG_004 = 'ONG_004', } type OngHubExceptionCodeType = keyof typeof OngHubExceptionCodes; @@ -25,4 +26,8 @@ export const OngHubExceptionMessages: Record< message: 'There was unexpected issue while requesting data from ONG Hub', code_error: OngHubExceptionCodes.ONG_003, }, + [OngHubExceptionCodes.ONG_004]: { + message: 'Could not update organization with the data from ONG Hub', + code_error: OngHubExceptionCodes.ONG_004, + }, }; diff --git a/backend/src/modules/organization/entities/organization.entity.ts b/backend/src/modules/organization/entities/organization.entity.ts index 6a012f1c1..9e80ccc7e 100644 --- a/backend/src/modules/organization/entities/organization.entity.ts +++ b/backend/src/modules/organization/entities/organization.entity.ts @@ -29,6 +29,19 @@ export class OrganizationEntity extends BaseEntity { @Column({ type: 'text', name: 'logo', nullable: true }) logo: string; + @Column({ type: 'text', name: 'cui', nullable: true }) + cui: string; + + @Column({ + type: 'text', + name: 'legal_representative_full_name', + nullable: true, + }) + legalReprezentativeFullName: string; + + @Column({ type: 'text', name: 'legal_representative_role', nullable: true }) + legalReprezentativeRole: string; + @OneToMany(() => VolunteerEntity, (volunteer) => volunteer.organization) volunteers: VolunteerEntity[]; diff --git a/backend/src/modules/organization/interfaces/organization-repository.interface.ts b/backend/src/modules/organization/interfaces/organization-repository.interface.ts index a895fd9ad..b5d6f9622 100644 --- a/backend/src/modules/organization/interfaces/organization-repository.interface.ts +++ b/backend/src/modules/organization/interfaces/organization-repository.interface.ts @@ -4,6 +4,7 @@ import { ICreateOrganizationModel, IFindOrganizationModel, IOrganizationModel, + IUpdateOrganizationModel, } from '../models/organization.model'; import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class'; import { OrganizationEntity } from '../entities/organization.entity'; @@ -15,7 +16,10 @@ import { IOrganizationVolunteerModel } from '../models/organization-volunteer.mo export interface IOrganizationRepository extends IRepositoryWithPagination { create(organization: ICreateOrganizationModel): Promise; - update(id: string, description: string): Promise; + update( + id: string, + updates: IUpdateOrganizationModel, + ): Promise; find( options: | Partial diff --git a/backend/src/modules/organization/models/organization.model.ts b/backend/src/modules/organization/models/organization.model.ts index cd43a2e72..d12d1f4fa 100644 --- a/backend/src/modules/organization/models/organization.model.ts +++ b/backend/src/modules/organization/models/organization.model.ts @@ -10,9 +10,13 @@ export interface IOrganizationModel { activityArea: string; logo: string; description: string; + cui?: string; + legalReprezentativeFullName?: string; + legalReprezentativeRole?: string; } export type ICreateOrganizationModel = Omit; +export type IUpdateOrganizationModel = Partial; export type IFindOrganizationModel = Pick< IOrganizationModel, @@ -37,6 +41,10 @@ export class OrganizationTransformer { activityArea: organizationEntity.activityArea, logo: organizationEntity.logo, description: organizationEntity.description, + cui: organizationEntity.cui, + legalReprezentativeFullName: + organizationEntity.legalReprezentativeFullName, + legalReprezentativeRole: organizationEntity.legalReprezentativeRole, }; } @@ -51,6 +59,11 @@ export class OrganizationTransformer { organizationEntity.activityArea = organizationModel.activityArea; organizationEntity.logo = organizationModel.logo; organizationEntity.description = organizationModel.description; + organizationEntity.cui = organizationModel.cui; + organizationEntity.legalReprezentativeFullName = + organizationModel.legalReprezentativeFullName; + organizationEntity.legalReprezentativeRole = + organizationModel.legalReprezentativeRole; return organizationEntity; } } diff --git a/backend/src/modules/organization/repositories/organization.repository.ts b/backend/src/modules/organization/repositories/organization.repository.ts index bbe7b7532..1ffdc3382 100644 --- a/backend/src/modules/organization/repositories/organization.repository.ts +++ b/backend/src/modules/organization/repositories/organization.repository.ts @@ -9,6 +9,7 @@ import { ICreateOrganizationModel, IFindOrganizationModel, IOrganizationModel, + IUpdateOrganizationModel, OrganizationTransformer, } from '../models/organization.model'; import { @@ -62,10 +63,10 @@ export class OrganizationRepositoryService public async update( id: string, - description: string, + updates: IUpdateOrganizationModel, ): Promise { // update organization entity - await this.organizationRepository.update({ id }, { description }); + await this.organizationRepository.update({ id }, { ...updates }); // return organization model return this.find({ id }); diff --git a/backend/src/modules/organization/services/organization.facade.ts b/backend/src/modules/organization/services/organization.facade.ts index 30d8e079b..ee9bbe638 100644 --- a/backend/src/modules/organization/services/organization.facade.ts +++ b/backend/src/modules/organization/services/organization.facade.ts @@ -5,6 +5,7 @@ import { ICreateOrganizationModel, IFindOrganizationModel, IOrganizationModel, + IUpdateOrganizationModel, } from '../models/organization.model'; import { OrganizationRepositoryService } from '../repositories/organization.repository'; import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class'; @@ -31,7 +32,14 @@ export class OrganizationFacadeService { organizationId: string, description: string, ): Promise { - return this.organizationRepository.update(organizationId, description); + return this.organizationRepository.update(organizationId, { description }); + } + + public async updateOrganization( + organizationId: string, + updates: IUpdateOrganizationModel, + ): Promise { + return this.organizationRepository.update(organizationId, updates); } public async createOrganization( diff --git a/backend/src/usecases/organization/sync-with-ngohub.usecase.ts b/backend/src/usecases/organization/sync-with-ngohub.usecase.ts new file mode 100644 index 000000000..867129af7 --- /dev/null +++ b/backend/src/usecases/organization/sync-with-ngohub.usecase.ts @@ -0,0 +1,69 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; +import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service'; +import { OngHubExceptionMessages } from 'src/modules/onghub/exceptions/exceptions'; +import { OngHubService } from 'src/modules/onghub/services/ong-hub.service'; +import { IOrganizationModel } from 'src/modules/organization/models/organization.model'; +import { OrganizationFacadeService } from 'src/modules/organization/services/organization.facade'; + +@Injectable() +export class SyncWithOngHubUseCaseService + implements IUseCaseService +{ + private readonly logger = new Logger(SyncWithOngHubUseCaseService.name); + + constructor( + private readonly ongHubService: OngHubService, + private readonly organizationService: OrganizationFacadeService, + private readonly exceptionService: ExceptionsService, + ) {} + + /** + * Synchronizes the organization data with ONG Hub. + * + * @param organizationId - The ID of the organization to be synchronized. + * @param token - The authentication token for ONG Hub which is the same as the one from VIC because we share the same Cognito User Pool. + * @returns A Promise that resolves to the updated IOrganizationModel. + * @throws InternalServerErrorException if there's an error fetching data from ONG Hub or updating the organization. + */ + async execute( + organizationId: string, + token: string, + ): Promise { + const userWithOrganization = + await this.ongHubService.getUserAndOrganizationDataFromOngHub(token); + + if (!userWithOrganization || !userWithOrganization.organization) { + this.exceptionService.internalServerErrorException( + OngHubExceptionMessages.ONG_002, + ); + } + + try { + const organization = await this.organizationService.updateOrganization( + organizationId, + { + name: userWithOrganization.organization.name, + email: userWithOrganization.organization.email, + phone: userWithOrganization.organization.phone, + address: userWithOrganization.organization.address, + activityArea: userWithOrganization.organization.activityArea, + logo: userWithOrganization.organization.logo, + description: userWithOrganization.organization.description, + cui: userWithOrganization.organization.cui, + legalReprezentativeFullName: + userWithOrganization.organization.legalReprezentativeFullName, + legalReprezentativeRole: + userWithOrganization.organization.legalReprezentativeRole, + }, + ); + + return organization; + } catch (error) { + console.log('[ONGHub Sync] Error updating organization:', error); + this.exceptionService.internalServerErrorException( + OngHubExceptionMessages.ONG_004, + ); + } + } +} diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts index 12fa37910..a595ac54d 100644 --- a/backend/src/usecases/use-case.module.ts +++ b/backend/src/usecases/use-case.module.ts @@ -137,6 +137,7 @@ import { SyncUserOrganizationsUsecase } from './user/sync-user-organizations.use import { GetRejectedAccessRequestUsecase } from './access-request/get-rejected-access-request.usecase'; import { DeleteAccountRegularUserUsecase } from './user/delete-account.usecase'; import { GeneratePDFsUseCase } from './documents/generate-pdfs.usecase'; +import { SyncWithOngHubUseCaseService } from './organization/sync-with-ngohub.usecase'; @Module({ imports: [ @@ -165,6 +166,7 @@ import { GeneratePDFsUseCase } from './documents/generate-pdfs.usecase'; SwitchOrganizationUsecase, LeaveOrganizationUsecase, RejoinOrganizationUsecase, + SyncWithOngHubUseCaseService, // Access Codes CreateAccessCodeUseCase, UpdateAccessCodeUseCase, @@ -310,6 +312,7 @@ import { GeneratePDFsUseCase } from './documents/generate-pdfs.usecase'; SwitchOrganizationUsecase, LeaveOrganizationUsecase, RejoinOrganizationUsecase, + SyncWithOngHubUseCaseService, // Access Codes CreateAccessCodeUseCase, UpdateAccessCodeUseCase, diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json index 679c4ebcb..ecf5caaf4 100644 --- a/frontend/src/assets/locales/en/translation.json +++ b/frontend/src/assets/locales/en/translation.json @@ -285,6 +285,9 @@ "subtitle": "Data taken from ONGHub.", "logo": "Organization logo", "name": "Organization name", + "cui": "CUI", + "legal_representative_full_name": "Legal representative name", + "legal_representative_role": "Legal representative role", "description": "Organization description", "vic_description": "Description of the organization in VIC", "description_placeholder": "The organization description is displayed in the organization profile in the VIC application. You can choose to formulate a specific description for the VIC app, which will attract as many volunteers as possible, or you can use the organization description as it is in ONGHub.", @@ -972,4 +975,4 @@ "confirm": "Confirm and sign" } } -} \ No newline at end of file +} diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 9dfe0f15f..80a67255e 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -285,6 +285,9 @@ "subtitle": "Datele sunt preluate din ONGHub.", "logo": "Logo organizație", "name": "Denumire organizație", + "cui": "CUI", + "legal_representative_full_name": "Nume reprezentant legal", + "legal_representative_role": "Rol reprezentant legal", "description": "Descriere organizație", "vic_description": "Descrierea organizației în VIC", "description_placeholder": "Descrierea organizației este afișată în profilul organizației din aplicația VIC. Poți alege să formulezi o descriere specifica pentru aplicația VIC, prin care să atragi cat mai multi voluntari, sau să folosești descrierea organizației asa cum se regăsește în ONGHub.", @@ -972,4 +975,4 @@ "confirm": "Confirmă și semnează" } } -} \ No newline at end of file +} diff --git a/frontend/src/common/interfaces/organization.interface.ts b/frontend/src/common/interfaces/organization.interface.ts index 72e4d4979..e4f24e8d5 100644 --- a/frontend/src/common/interfaces/organization.interface.ts +++ b/frontend/src/common/interfaces/organization.interface.ts @@ -7,4 +7,7 @@ export interface IOrganization { activityArea: string; logo: string; description: string; + cui: string; + legalReprezentativeFullName: string; + legalReprezentativeRole: string; } diff --git a/frontend/src/pages/Organization.tsx b/frontend/src/pages/Organization.tsx index 95abbfd44..182fb6e45 100644 --- a/frontend/src/pages/Organization.tsx +++ b/frontend/src/pages/Organization.tsx @@ -1,7 +1,10 @@ import React, { useEffect } from 'react'; import { useErrorToast } from '../hooks/useToast'; import PageLayout from '../layouts/PageLayout'; -import { useOrganizationQuery } from '../services/organization/organization.service'; +import { + useOrganizationQuery, + useResyncOrganizationWithOngHubMutation, +} from '../services/organization/organization.service'; import { InternalErrors } from '../common/errors/internal-errors.class'; import i18n from '../common/config/i18n'; import EmptyContent from '../components/EmptyContent'; @@ -22,6 +25,7 @@ import FormInput from '../components/FormInput'; import FormTextarea from '../components/FormTextarea'; import DivisionTable from '../containers/query/DivisionTableWithQueryParams'; import { OrganizationTableProps } from '../containers/query/OrganizationWithQueryParam'; +import { useQueryClient } from 'react-query'; export const DivisionsTabs: SelectItem[] = [ { key: DivisionType.BRANCH, value: i18n.t(`division:table.title.branch`) }, @@ -31,6 +35,7 @@ export const DivisionsTabs: SelectItem[] = [ const Organization = ({ query, setQuery }: OrganizationTableProps) => { const navigate = useNavigate(); + const queryClient = useQueryClient(); const { data: organization, @@ -38,6 +43,11 @@ const Organization = ({ query, setQuery }: OrganizationTableProps) => { isLoading: isOrganizationLoading, } = useOrganizationQuery(); + const { + mutate: resyncOrganizationWithOngHub, + isLoading: isResyncingOrganizationWithOngHubLoading, + } = useResyncOrganizationWithOngHubMutation(); + // error handling useEffect(() => { // map error messages for ORGANIZATION fetch @@ -81,12 +91,25 @@ const Organization = ({ query, setQuery }: OrganizationTableProps) => {

{i18n.t('organization:title.view')}

-
@@ -106,6 +129,21 @@ const Organization = ({ query, setQuery }: OrganizationTableProps) => { value={organization.name} readOnly /> + + + => { return API.patch(`/organization`, { description }).then((res) => res.data); }; +export const resyncOrganizationWithOngHub = async (): Promise => { + return API.post(`/organization/onghub/sync`).then((res) => res.data); +}; export const getAccessCodes = async ( limit: number, diff --git a/frontend/src/services/organization/organization.service.ts b/frontend/src/services/organization/organization.service.ts index cc6171f96..676cc9655 100644 --- a/frontend/src/services/organization/organization.service.ts +++ b/frontend/src/services/organization/organization.service.ts @@ -10,6 +10,7 @@ import { getAccessCode, getAccessCodes, getOrganization, + resyncOrganizationWithOngHub, updateAccessCode, updateOrganizationDescription, } from './organization.api'; @@ -31,6 +32,12 @@ export const useUpdateOrganizationDescriptionMutation = () => { ); }; +export const useResyncOrganizationWithOngHubMutation = () => { + return useMutation(['resync-organization-onghub'], () => resyncOrganizationWithOngHub(), { + onError: (error: AxiosError>) => Promise.resolve(error), + }); +}; + export const useAccessCodesQuery = ( limit: number, page: number, From 93ba2ec0bbdfa5dd993b8775fe5d6d24c88d1273 Mon Sep 17 00:00:00 2001 From: luciatugui Date: Tue, 3 Sep 2024 16:57:09 +0300 Subject: [PATCH 10/55] wip: [Contracts] - stepper component & expandable card --- frontend/package-lock.json | 22 ++ frontend/package.json | 2 + .../src/assets/locales/en/translation.json | 15 +- .../src/assets/locales/ro/translation.json | 17 +- frontend/src/common/constants/routes.ts | 5 + frontend/src/components/CardsExample.tsx | 117 +++++++++++ frontend/src/components/ContentExpander.tsx | 70 +++++++ frontend/src/components/ContractCard.tsx | 193 ++++++++++++++++++ .../src/components/ContractCardHeader.tsx | 31 +++ frontend/src/components/DateRangePicker.tsx | 10 +- frontend/src/components/Signatures.tsx | 54 ++++- frontend/src/components/Stepper.tsx | 51 +++++ .../contracts/ContractTermsContent.tsx | 3 +- frontend/src/index.css | 4 + frontend/src/pages/StepperExample.tsx | 68 ++++++ frontend/src/routes/Router.tsx | 4 +- 16 files changed, 650 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/CardsExample.tsx create mode 100644 frontend/src/components/ContentExpander.tsx create mode 100644 frontend/src/components/ContractCard.tsx create mode 100644 frontend/src/components/ContractCardHeader.tsx create mode 100644 frontend/src/components/Stepper.tsx create mode 100644 frontend/src/pages/StepperExample.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fc53dc702..1c0c41e04 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "aws-amplify": "^6.4.0", "axios": "^1.7.2", "date-fns": "^3.6.0", + "dompurify": "^3.1.6", "i18next": "23.11.5", "lodash.debounce": "^4.0.8", "react": "18.3.1", @@ -39,6 +40,7 @@ "devDependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.7", + "@types/dompurify": "^3.0.5", "@types/feather-icons": "^4.29.4", "@types/lodash.debounce": "^4.0.9", "@types/node": "^20.14.10", @@ -4441,6 +4443,15 @@ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -4560,6 +4571,12 @@ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", "peer": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -6241,6 +6258,11 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" + }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 512480ff9..718d2be72 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "aws-amplify": "^6.4.0", "axios": "^1.7.2", "date-fns": "^3.6.0", + "dompurify": "^3.1.6", "i18next": "23.11.5", "lodash.debounce": "^4.0.8", "react": "18.3.1", @@ -60,6 +61,7 @@ "devDependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.7", + "@types/dompurify": "^3.0.5", "@types/feather-icons": "^4.29.4", "@types/lodash.debounce": "^4.0.9", "@types/node": "^20.14.10", diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json index 337fa258f..02c96334c 100644 --- a/frontend/src/assets/locales/en/translation.json +++ b/frontend/src/assets/locales/en/translation.json @@ -87,7 +87,9 @@ }, "confirm": "Confirm", "no_options": "No options", - "type_for_options": "Type for options..." + "type_for_options": "Type for options...", + "hide": "Show less", + "show": "Show more" }, "header": { "login": "Login", @@ -1050,6 +1052,10 @@ }, "p4": "it was agreed to conclude this contract under Law no. 78/2014 regarding the regulation of volunteering activity in Romania." }, + "contract_data": "Contract data", + "contract_no": "Contract number", + "contract_date": "Contract date", + "contract_period": "Contract period", "contract_duration": { "title": "CONTRACT DURATION", "description": "This contract is concluded for a fixed period, between", @@ -1079,5 +1085,12 @@ "legal_rep_name": "Legal representative's name", "number": "Number", "telephone": "Phone" + }, + "stepper": { + "choose_template": "Choose template", + "choose_volunteers": "Choose volunteers", + "fill": "Fill", + "attachments": "Attachments", + "complete": "Complete" } } \ No newline at end of file diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index d2c06ac05..eb7b977db 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -87,7 +87,9 @@ }, "confirm": "Confirmă", "no_options": "Fără opțiuni", - "type_for_options": "Începeți să scrieți pentru opțiuni..." + "type_for_options": "Începeți să scrieți pentru opțiuni...", + "hide": "Vezi mai puțin", + "show": "Vezi tot" }, "header": { "login": "Login", @@ -1048,8 +1050,12 @@ "named": "numit în continuare", "volunteer": "Voluntar," }, - "p4": "s-a convenit încheierea prezentului contract în baza Legii nr. 78/2014 privind reglementarea activității de voluntariat din Romănia." + "p4": "s-a convenit încheierea prezentului contract în baza Legii nr. 78/2014 privind reglementarea activității de voluntariat din România." }, + "contract_data": "Datele contractului", + "contract_no": "Număr contract", + "contract_date": "Data contractlui", + "contract_period": "Perioadă contract", "contract_duration": { "title": "DURATA CONTRACTULUI", "description": "Prezentul contract se încheie pe o perioadă determinată, între data de", @@ -1079,5 +1085,12 @@ "legal_rep_name": "Nume reprezentant legal", "number": "Număr", "telephone": "Telefon" + }, + "stepper": { + "choose_template": "Alege template", + "choose_volunteers": "Alege voluntari", + "fill": "Completează", + "attachments": "Anexe", + "complete": "Finalizează" } } \ No newline at end of file diff --git a/frontend/src/common/constants/routes.ts b/frontend/src/common/constants/routes.ts index 37c9424c4..0fbcdd292 100644 --- a/frontend/src/common/constants/routes.ts +++ b/frontend/src/common/constants/routes.ts @@ -71,6 +71,11 @@ export const ROUTES: IRoute[] = [ name: i18n.t('general:templates'), href: 'documents/templates', }, + { + id: 63, + name: 'stepper', + href: 'documents/templates/stepper_example', + }, ], }, { diff --git a/frontend/src/components/CardsExample.tsx b/frontend/src/components/CardsExample.tsx new file mode 100644 index 000000000..ce1fc6722 --- /dev/null +++ b/frontend/src/components/CardsExample.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { ContractCard } from './ContractCard'; + +const items = [ + { + contract: { + id: '0', + name: 'Contract 0', + }, + volunteer: { + name: 'Emma Wilson', + address: '123 Oak St, Springfield', + cnp: '1234567890123', + series: 'AB', + number: '123456', + institution: 'Springfield Police Department', + issuanceDate: '2022-01-15', + image: 'https://randomuser.me/api/portraits/women/0.jpg', + }, + }, + { + contract: { + id: '1', + name: 'Contract 1', + }, + volunteer: { + name: 'John Doe', + address: '456 Elm St, Shelbyville', + cnp: '2345678901234', + series: 'CD', + number: '234567', + institution: 'Shelbyville City Hall', + issuanceDate: '2021-11-30', + image: 'https://randomuser.me/api/portraits/men/1.jpg', + legalRepresentative: { + name: 'Mariam Abbott', + series: 'ABC', + no: 123, + tel: '0755675678', + }, + }, + }, + { + contract: { + id: '2', + name: 'Contract 2', + }, + volunteer: { + name: 'Jane Smith', + address: '789 Maple Ave, Capital City', + cnp: '3456789012345', + series: 'EF', + number: '345678', + institution: 'Capital City Police Department', + issuanceDate: '2023-03-22', + image: 'https://randomuser.me/api/portraits/women/2.jpg', + }, + }, + { + contract: { + id: '3', + name: 'Contract 3', + }, + volunteer: { + name: 'Alice Johnson', + address: '101 Pine Rd, Oakville', + cnp: '4567890123456', + series: 'GH', + number: '456789', + institution: 'Oakville Municipal Office', + issuanceDate: '2022-07-10', + image: 'https://randomuser.me/api/portraits/women/3.jpg', + }, + }, + { + contract: { + id: '4', + name: 'Contract 4', + }, + volunteer: { + name: 'Bob Brown', + address: '202 Cedar Ln, Rivertown', + cnp: '5678901234567', + series: 'IJ', + number: '567890', + institution: 'Rivertown Police Station', + issuanceDate: '2023-01-05', + image: 'https://randomuser.me/api/portraits/men/4.jpg', + }, + }, + { + contract: { + id: '5', + name: 'Contract 5', + }, + volunteer: { + name: 'Charlie Davis', + address: '303 Birch Blvd, Hillside', + cnp: '6789012345678', + series: 'KL', + number: '678901', + institution: 'Hillside City Hall', + issuanceDate: '2022-09-18', + image: 'https://randomuser.me/api/portraits/men/5.jpg', + }, + }, +]; + +export const CardsExample = () => { + return ( +
+ {items.map((item) => ( + + ))} +
+ ); +}; diff --git a/frontend/src/components/ContentExpander.tsx b/frontend/src/components/ContentExpander.tsx new file mode 100644 index 000000000..f8e4a20d7 --- /dev/null +++ b/frontend/src/components/ContentExpander.tsx @@ -0,0 +1,70 @@ +import React, { useEffect, useRef, useState } from 'react'; +import Button from './Button'; +import DOMPurify from 'dompurify'; +import { useTranslation } from 'react-i18next'; + +interface ContentExpanderProps { + fullContent: string; + maxHeight?: number; +} + +/** + * This component renders expandable content with a "Show More/Less" functionality. + * + * @param {string} fullContent - The full HTML content to be displayed/expanded + * @param {number} maxHeight - The maximum height (in pixels) before content is truncated + * + * + * Features: + * - Automatically detects if content needs expansion (height > 144px by default) + * - Truncates content with a gradient overlay when collapsed + * - Toggles between expanded and collapsed states + * - Sanitizes HTML content for safe rendering + * + * @returns {JSX.Element} Rendered ContentExpander component + */ + +export const ContentExpander = ({ fullContent, maxHeight = 144 }: ContentExpanderProps) => { + const { t } = useTranslation('general'); + const [isExpanded, setIsExpanded] = useState(false); + const [needsExpansion, setNeedsExpansion] = useState(false); + const contentRef = useRef(null); + + // see if the content inside has a bigger height than 144px + // we use this to decide if we should display the see more/less button + useEffect(() => { + if (contentRef.current) { + setNeedsExpansion(contentRef.current.scrollHeight > maxHeight); // h-36 = 36 * 4 = 144px + } + }, [fullContent]); + + const toggleExpand = () => { + setIsExpanded(!isExpanded); + }; + + return ( +
+
+
+ + {/* fade to white overlay*/} + {needsExpansion && !isExpanded && ( +
+ )} +
+ {needsExpansion && ( + + )} +
+ ); +}; diff --git a/frontend/src/components/ContractCard.tsx b/frontend/src/components/ContractCard.tsx new file mode 100644 index 000000000..ccd3bcdf1 --- /dev/null +++ b/frontend/src/components/ContractCard.tsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react'; +import FormInput from './FormInput'; +import Button from './Button'; +import { Controller, FieldValues, useForm } from 'react-hook-form'; +import FormDatePicker from './FormDatePicker'; +import DateRangePicker from './DateRangePicker'; +import { ContractCardHeader } from './ContractCardHeader'; +import { useTranslation } from 'react-i18next'; +import { useOrganizationQuery } from '../services/organization/organization.service'; +import { Signatures } from './Signatures'; +import { ContentExpander } from './ContentExpander'; + +export interface IMockContract { + id: string; + name: string; +} + +export interface IMockVolunteer { + name: string; + address: string; + cnp: string; + series: string; + number: string; + institution: string; + issuanceDate: string; + image: string; + legalRepresentative?: { + name: string; + series: string; + no: number; + tel: string; + }; +} + +interface ContractCardProps { + data: { contract: IMockContract; volunteer: IMockVolunteer }; +} + +const dotsString = '.........................'; +const contractTerms = + '

h1

h2

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quis lobortis nisl cursus bibendum sit nulla accumsan sodales ornare. At urna viverra non suspendisse neque, lorem. Pretium condimentum pellentesque gravida id etiam sit sed arcu euismod. Rhoncus proin orci duis scelerisque molestie cursus tincidunt aliquam.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quis lobortis nisl cursus bibendum sit nulla accumsan sodales ornare. At urna viverra non suspendisse neque, lorem. Pretium condimentum pellentesque gravida id etiam sit sed arcu euismod. Rhoncus proin orci duis scelerisque molestie cursus tincidunt aliquam.

'; + +export const ContractCard = ({ data }: ContractCardProps) => { + const { t } = useTranslation(['doc_templates', 'general']); + //? todo: get the contract data from a query and not from props + const { contract, volunteer } = data; + // todo: delete this + console.log(contract); + + // contract card states + const [open, setOpen] = useState(false); + const [edit, setEdit] = useState(false); + + const [contractNumber, setContractNumber] = useState(dotsString); + const [contractDate, setContractDate] = useState(dotsString); + const [contractPeriod, setContractPeriod] = useState([dotsString, dotsString]); + + const { data: organization } = useOrganizationQuery(); + const { control, handleSubmit, setValue } = useForm(); + + const onSubmit = (data: FieldValues) => { + if (data.contractNumber) { + setContractNumber(data.contractNumber); + } + + if (data.contractDate) { + setContractDate(data.contractDate.toLocaleDateString()); + } + + if (data.contractPeriod) { + setContractPeriod([ + data.contractPeriod[0].toLocaleDateString(), + data.contractPeriod[1].toLocaleDateString(), + ]); + } + + setEdit(false); + }; + + return ( +
+ + + {open && ( +
+ {/* datele contractului */} +
+

{t('contract_data')}

+ {/* //todo: icon at the end of the input??? */} + ( + + )} + /> + ( + + )} + /> + + {/* no controller here, as the value of this component is managed differently inside */} + setValue('contractPeriod', value)} + /> + + {/* //? ce facem dupa ce editam astea? */} +
+ + {/* contract preview */} +
+

{t('template_preview.title')}

+

+ {t('template_preview.p1.no')} {contractNumber} {t('template_preview.p1.date')}{' '} + {contractDate} +

+ +

+ {t('template_preview.p2.between')}{' '} + {organization?.name || ''}{' '} + {t('template_preview.p2.address')} {organization?.address || ''}{' '} + {t('template_preview.p2.identified')} + {organization?.cui || '!!!!'} + {', '} + {t('template_preview.p2.represented_by')}{' '} + + {' '} + {organization?.legalRepresentative || '!!!!!!'} + + {', '} + {t('template_preview.p2.as')} {organization?.legalRepresentativeRole || '!!!!!!!!!'}{' '} + {t('template_preview.p2.named')}{' '} + {t('template_preview.p2.organization')}{' '} +

+ +

{t('template_preview.and')}

+ +

+ {volunteer.name},{' '} + {t('template_preview.p3.lives')} {volunteer.address}, {t('template_preview.p3.cnp')}{' '} + {volunteer.cnp},{' '} + {t('template_preview.p3.legitimate')} {volunteer.series} {t('template_preview.p3.no')}{' '} + {volunteer.number}, {t('template_preview.p3.by')} {volunteer.institution},{' '} + {t('template_preview.p3.at_date')} {volunteer.issuanceDate} + {', '} + {t('template_preview.p3.named')}{' '} + {t('template_preview.p3.volunteer')}{' '} +

+ +

{t('template_preview.p4')}

+ + {/* P5: DURATA CONTRACTULUI */} +

{t('contract_duration.title')}

+

+ {t('contract_duration.description')} {contractPeriod[0]} {t('template_preview.and')}{' '} + {contractPeriod[1]}. +

+ +
+

{t('contract_terms.title')}

+ + {contractTerms && } +
+ +
+
+ )} +
+ ); +}; diff --git a/frontend/src/components/ContractCardHeader.tsx b/frontend/src/components/ContractCardHeader.tsx new file mode 100644 index 000000000..1cb4de401 --- /dev/null +++ b/frontend/src/components/ContractCardHeader.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { TrashIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid'; +import { IMockVolunteer } from './ContractCard'; + +interface ContractCardHeaderProps { + open: boolean; + setOpen: React.Dispatch>; + volunteer: IMockVolunteer; + // todo: onDelete +} + +export const ContractCardHeader = ({ open, setOpen, volunteer }: ContractCardHeaderProps) => { + return ( +
setOpen(!open)} + > + console.log('delete item')} /> +
+ +
+ {volunteer.name} + {open ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/frontend/src/components/DateRangePicker.tsx b/frontend/src/components/DateRangePicker.tsx index caa9046e0..b05d4e71b 100644 --- a/frontend/src/components/DateRangePicker.tsx +++ b/frontend/src/components/DateRangePicker.tsx @@ -10,16 +10,19 @@ interface DateRangePickerProps { label: string; value?: Date[]; onChange?: (range: Date[]) => void; + disabled?: boolean; } -const DateRangePicker = ({ label, value, onChange, id }: DateRangePickerProps) => { - const [dateRange, setDateRange] = useState([]); +const DateRangePicker = ({ label, value, onChange, id, disabled }: DateRangePickerProps) => { + const [dateRange, setDateRange] = useState(value || []); const [startDate, endDate] = dateRange; useEffect(() => { if (value) { setDateRange(value); - } else [setDateRange([])]; + } else { + setDateRange([]); + } }, [value]); useEffect(() => { @@ -54,6 +57,7 @@ const DateRangePicker = ({ label, value, onChange, id }: DateRangePickerProps) = isClearable={false} placeholderText={i18n.t('general:select_interval').toString()} id={`${id}__date-picker`} + disabled={disabled} />
diff --git a/frontend/src/components/Signatures.tsx b/frontend/src/components/Signatures.tsx index a2520d0dc..2511f0cc3 100644 --- a/frontend/src/components/Signatures.tsx +++ b/frontend/src/components/Signatures.tsx @@ -3,8 +3,13 @@ import { InfoParagraph } from './InfoParagraph'; import { Signature } from './Signature'; import { useTranslation } from 'react-i18next'; import { useOrganizationQuery } from '../services/organization/organization.service'; +import { IMockVolunteer } from './ContractCard'; -export const Signatures = () => { +interface SignatureProps { + volunteer?: IMockVolunteer; +} + +export const Signatures = ({ volunteer }: SignatureProps) => { const { t } = useTranslation('doc_templates'); const { data: organization } = useOrganizationQuery(); @@ -15,19 +20,31 @@ export const Signatures = () => { signatureTitle={
{t('template_preview.p2.organization')}{' '} - + {volunteer ? ( +

{organization?.name}

+ ) : ( + + )}
} p={
- {t('represented_by')} + {' '} + {t('represented_by')} + {volunteer ?

{organization?.legalRepresentative}

: }
} /> {t('volunteer.volunteer')}

} - p={} + p={ + volunteer ? ( + volunteer.name + ) : ( + + ) + } /> @@ -39,13 +56,34 @@ export const Signatures = () => { signatureTitle={

{t('legal_representative')}

} p={
- + {volunteer?.legalRepresentative?.name ? ( +

{volunteer.legalRepresentative.name}

+ ) : ( + + )} +
- {t('identification')} ,{' '} - + {t('identification')} + {volunteer?.legalRepresentative?.series ? ( +

{volunteer.legalRepresentative.series}

+ ) : ( + + )} + , + {volunteer?.legalRepresentative?.no ? ( +

{volunteer?.legalRepresentative?.no}

+ ) : ( + + )}
- {t('tel_no')} + {t('tel_no')} + + {volunteer?.legalRepresentative?.tel ? ( +

{volunteer.legalRepresentative.tel}

+ ) : ( + + )}
} diff --git a/frontend/src/components/Stepper.tsx b/frontend/src/components/Stepper.tsx new file mode 100644 index 000000000..d4b4814cb --- /dev/null +++ b/frontend/src/components/Stepper.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { CheckIcon } from '@heroicons/react/24/outline'; + +interface Step { + id: string; + label: string; +} +interface StepperProps { + steps: Step[]; + currentStep: number; + completedSteps: boolean[]; + goToStep: (step: number) => void; +} + +const activeStyles = 'border-yellow text-yellow'; +const completedStyles = 'border-yellow bg-yellow'; + +export const Stepper = ({ steps, currentStep, completedSteps, goToStep }: StepperProps) => { + return ( +
+ {steps.map((step, index) => { + const isActive = index === currentStep; + const isCompleted = completedSteps[index]; + + return ( +
goToStep(index)} + className={`flex-1 flex flex-row gap-2 items-center justify-center ${isCompleted ? 'hover:cursor-pointer' : 'cursor-not-allowed'}`} + > +
+ {/* show check icon if completed, otherwise show step number with '0' in front if step < 9 -> (01, 02, ..., 09, 10) */} + {isCompleted ? ( + + ) : ( + `${index < 9 ? '0' : ''}${index + 1}` + )} +
+

+ {step.label} +

+
+ ); + })} +
+ ); +}; diff --git a/frontend/src/components/contracts/ContractTermsContent.tsx b/frontend/src/components/contracts/ContractTermsContent.tsx index 154b76e0f..47ffb566e 100644 --- a/frontend/src/components/contracts/ContractTermsContent.tsx +++ b/frontend/src/components/contracts/ContractTermsContent.tsx @@ -2,6 +2,7 @@ import React from 'react'; import Button from '../Button'; import { PencilIcon } from '@heroicons/react/24/solid'; import { useTranslation } from 'react-i18next'; +import DOMPurify from 'dompurify'; export const ContractTermsContent = ({ innerContent, @@ -23,7 +24,7 @@ export const ContractTermsContent = ({ onClick={() => setEditingText(true)} /> -
+
); }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 43bfca06c..09d7fea37 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -330,6 +330,10 @@ li.recharts-legend-item { @apply flex items-center !important; } +.react-datepicker-popper { + z-index: 1000; +} + @layer utilities { /* Hide scrollbar for Chrome, Safari, and Opera */ .no-scrollbar::-webkit-scrollbar { diff --git a/frontend/src/pages/StepperExample.tsx b/frontend/src/pages/StepperExample.tsx new file mode 100644 index 000000000..38e67f09d --- /dev/null +++ b/frontend/src/pages/StepperExample.tsx @@ -0,0 +1,68 @@ +import React, { useMemo, useState } from 'react'; +import { Stepper } from '../components/Stepper'; +import Button from '../components/Button'; +import { CardsExample } from '../components/CardsExample'; +import { useTranslation } from 'react-i18next'; + +export const StepperExample = () => { + const { t } = useTranslation('stepper'); + const steps = useMemo( + () => [ + { id: '1', label: t('choose_template') }, + { id: '2', label: t('choose_volunteers') }, + { id: '3', label: t('fill') }, + { id: '4', label: t('attachments') }, + { id: '5', label: t('complete') }, + ], + [], + ); + + //? do we take the step from the query params? + const [currentStep, setCurrentStep] = useState(0); + const [completedSteps, setCompletedSteps] = useState(new Array(steps.length).fill(false)); + + const goToStep = (index: number) => { + if (index <= currentStep) { + setCurrentStep(index); + } + }; + + const handleNext = () => { + // mark the current step as completed + const updatedSteps = [...completedSteps]; + updatedSteps[currentStep] = true; + setCompletedSteps(updatedSteps); + + // move to the next step if possible + if (currentStep < steps.length - 1) { + setCurrentStep(currentStep + 1); + } + }; + + const handlePrevious = () => { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } + }; + + return ( +
+

Stepper Example

+ +
+
+ +
+ ); +}; diff --git a/frontend/src/routes/Router.tsx b/frontend/src/routes/Router.tsx index 8d5db3f20..b9b70f561 100644 --- a/frontend/src/routes/Router.tsx +++ b/frontend/src/routes/Router.tsx @@ -35,7 +35,8 @@ import AddContractTemplate from '../pages/AddContractTemplate'; import EditContractTemplate from '../pages/EditContractTemplate'; import AddContract from '../containers/query/AddContractWithQueryParams'; import ActionsArchive from '../pages/ActionsArchive'; -import { ContractTemplates } from '../components/contracts/ContractTemplates'; +import { ContractTemplates } from '../pages/ContractTemplates'; +import { StepperExample } from '../pages/StepperExample'; const Router = () => { return ( @@ -95,6 +96,7 @@ const Router = () => { }> } /> + } /> From 73ddddda4ee5c459064a31cd13461a80ec3b7ee8 Mon Sep 17 00:00:00 2001 From: luciatugui Date: Tue, 3 Sep 2024 17:30:36 +0300 Subject: [PATCH 11/55] feat: [Contracts] - add error tooltip on expandable contract card --- .../src/components/ContractCardHeader.tsx | 69 +++++++++++++++---- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/ContractCardHeader.tsx b/frontend/src/components/ContractCardHeader.tsx index 1cb4de401..12818d122 100644 --- a/frontend/src/components/ContractCardHeader.tsx +++ b/frontend/src/components/ContractCardHeader.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { TrashIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid'; +import { ExclamationCircleIcon } from '@heroicons/react/24/outline'; import { IMockVolunteer } from './ContractCard'; +import { Tooltip } from 'react-tooltip'; interface ContractCardHeaderProps { open: boolean; @@ -10,22 +12,61 @@ interface ContractCardHeaderProps { } export const ContractCardHeader = ({ open, setOpen, volunteer }: ContractCardHeaderProps) => { + // todo: remove isError from here and use the right thing instead + const isError = true; + return ( -
setOpen(!open)} - > - console.log('delete item')} /> -
- +
+
console.log('delete item')} + > + +
+ +
setOpen(!open)} + > +
+ +
+ {volunteer.name} +
+ {isError && ( + <> + + + + )} + + {open ? ( + + ) : ( + + )} +
- {volunteer.name} - {open ? ( - - ) : ( - - )}
); }; From af9a7d21ea0aee2653f801662f074d21b25b00db Mon Sep 17 00:00:00 2001 From: luciatugui Date: Wed, 4 Sep 2024 10:13:16 +0300 Subject: [PATCH 12/55] wip: [Contracts] - fast contract data completion --- frontend/src/components/CardsExample.tsx | 28 +++++++++++++++---- .../src/components/ContractCardHeader.tsx | 2 +- frontend/src/components/DateRangePicker.tsx | 12 ++++++-- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/CardsExample.tsx b/frontend/src/components/CardsExample.tsx index ce1fc6722..a5eb5c4bd 100644 --- a/frontend/src/components/CardsExample.tsx +++ b/frontend/src/components/CardsExample.tsx @@ -1,5 +1,8 @@ import React from 'react'; import { ContractCard } from './ContractCard'; +import FormInput from './FormInput'; +import FormDatePicker from './FormDatePicker'; +import DateRangePicker from './DateRangePicker'; const items = [ { @@ -108,10 +111,25 @@ const items = [ export const CardsExample = () => { return ( -
- {items.map((item) => ( - - ))} -
+ <> +
+

Completare rapidă

+

+ Precompletează datele de mai jos dintr-un singur click! Poți modifica datele individual + ulterior. +

+
+ + + {/* //todo: something about this */} + +
+
+
+ {items.map((item) => ( + + ))} +
+ ); }; diff --git a/frontend/src/components/ContractCardHeader.tsx b/frontend/src/components/ContractCardHeader.tsx index 12818d122..5d939ff50 100644 --- a/frontend/src/components/ContractCardHeader.tsx +++ b/frontend/src/components/ContractCardHeader.tsx @@ -16,7 +16,7 @@ export const ContractCardHeader = ({ open, setOpen, volunteer }: ContractCardHea const isError = true; return ( -
+
console.log('delete item')} diff --git a/frontend/src/components/DateRangePicker.tsx b/frontend/src/components/DateRangePicker.tsx index b05d4e71b..acf028ea5 100644 --- a/frontend/src/components/DateRangePicker.tsx +++ b/frontend/src/components/DateRangePicker.tsx @@ -11,9 +11,17 @@ interface DateRangePickerProps { value?: Date[]; onChange?: (range: Date[]) => void; disabled?: boolean; + className?: string; } -const DateRangePicker = ({ label, value, onChange, id, disabled }: DateRangePickerProps) => { +const DateRangePicker = ({ + label, + value, + onChange, + id, + disabled, + className, +}: DateRangePickerProps) => { const [dateRange, setDateRange] = useState(value || []); const [startDate, endDate] = dateRange; @@ -36,7 +44,7 @@ const DateRangePicker = ({ label, value, onChange, id, disabled }: DateRangePick }; return ( -
+
{label && }
From 7c164601e0389d9bb107e7565cee887eec952b9d Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Wed, 4 Sep 2024 16:04:54 +0300 Subject: [PATCH 13/55] feat: [Contracts] Database and APIs --- backend/src/api/api.module.ts | 2 + .../documents/document-template.controller.ts | 49 ++++++++++++++ .../dto/create-document-template.dto.ts | 52 +++++++++++++++ .../presenters/document-template.presenter.ts | 52 +++++++++++++++ .../1725454854964-AddDocumentTemplate.ts | 33 ++++++++++ .../src/modules/documents/documents.module.ts | 14 +++- .../entities/document-template.entity.ts | 51 ++++++++++++++ .../documente-template.exceptions.ts | 23 +++++++ .../document-template-repository.interface.ts | 17 +++++ .../models/document-template.model.ts | 66 +++++++++++++++++++ .../document-template.repository.ts | 46 +++++++++++++ .../services/document-template.facade.ts | 26 ++++++++ .../create-document-template.usecase.ts | 39 +++++++++++ .../get-one-document-template.usecase.ts | 36 ++++++++++ backend/src/usecases/use-case.module.ts | 6 ++ 15 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 backend/src/api/documents/document-template.controller.ts create mode 100644 backend/src/api/documents/dto/create-document-template.dto.ts create mode 100644 backend/src/api/documents/presenters/document-template.presenter.ts create mode 100644 backend/src/migrations/1725454854964-AddDocumentTemplate.ts create mode 100644 backend/src/modules/documents/entities/document-template.entity.ts create mode 100644 backend/src/modules/documents/exceptions/documente-template.exceptions.ts create mode 100644 backend/src/modules/documents/interfaces/document-template-repository.interface.ts create mode 100644 backend/src/modules/documents/models/document-template.model.ts create mode 100644 backend/src/modules/documents/repositories/document-template.repository.ts create mode 100644 backend/src/modules/documents/services/document-template.facade.ts create mode 100644 backend/src/usecases/documents/create-document-template.usecase.ts create mode 100644 backend/src/usecases/documents/get-one-document-template.usecase.ts diff --git a/backend/src/api/api.module.ts b/backend/src/api/api.module.ts index 6483f3826..f7f8eb073 100644 --- a/backend/src/api/api.module.ts +++ b/backend/src/api/api.module.ts @@ -31,6 +31,7 @@ import { MobileStatisticsController } from './_mobile/statistics/statistics.cont import { MobileAnouncementsController } from './_mobile/anouncements/anouncements.controller'; import { MobileSettingsController } from './_mobile/settings/settings-controller'; import { MobileNewsController } from './_mobile/news/news.controller'; +import { DocumentTemplateController } from './documents/document-template.controller'; @Module({ imports: [UseCaseModule], @@ -52,6 +53,7 @@ import { MobileNewsController } from './_mobile/news/news.controller'; DashboardController, TemplateController, ContractController, + DocumentTemplateController, // Mobile MobileRegularUserController, MobileAccessRequestController, diff --git a/backend/src/api/documents/document-template.controller.ts b/backend/src/api/documents/document-template.controller.ts new file mode 100644 index 000000000..da5de3563 --- /dev/null +++ b/backend/src/api/documents/document-template.controller.ts @@ -0,0 +1,49 @@ +import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; +import { ExtractUser } from 'src/common/decorators/extract-user.decorator'; +import { IAdminUserModel } from 'src/modules/user/models/admin-user.model'; +import { CreateDocumentTemplateDto } from './dto/create-document-template.dto'; +import { ApiBearerAuth, ApiBody, ApiParam } from '@nestjs/swagger'; +import { DocumentTemplatePresenter } from './presenters/document-template.presenter'; +import { UuidValidationPipe } from 'src/infrastructure/pipes/uuid.pipe'; +import { CreateDocumentTemplateUsecase } from 'src/usecases/documents/create-document-template.usecase'; +import { WebJwtAuthGuard } from 'src/modules/auth/guards/jwt-web.guard'; +import { GetOneDocumentTemplateUseCase } from 'src/usecases/documents/get-one-document-template.usecase'; + +@ApiBearerAuth() +@UseGuards(WebJwtAuthGuard) +@Controller('documents/templates') +export class DocumentTemplateController { + constructor( + private readonly createDocumentTemplateUsecase: CreateDocumentTemplateUsecase, + private readonly getOneDocumentTemplateUsecase: GetOneDocumentTemplateUseCase, + ) {} + + @ApiBody({ type: CreateDocumentTemplateDto }) + @Post() + async create( + @Body() payload: CreateDocumentTemplateDto, + @ExtractUser() { organizationId, id: adminId }: IAdminUserModel, + ): Promise { + const newDocumentTemplate = + await this.createDocumentTemplateUsecase.execute({ + ...payload, + createdByAdminId: adminId, + organizationId, + }); + return new DocumentTemplatePresenter(newDocumentTemplate); + } + + @ApiParam({ name: 'id', type: 'string' }) + @Get(':id') + async getOne( + @Param('id', UuidValidationPipe) id: string, + @ExtractUser() { organizationId }: IAdminUserModel, + ): Promise { + const documentTemplate = await this.getOneDocumentTemplateUsecase.execute( + { id }, + organizationId, + ); + + return new DocumentTemplatePresenter(documentTemplate); + } +} diff --git a/backend/src/api/documents/dto/create-document-template.dto.ts b/backend/src/api/documents/dto/create-document-template.dto.ts new file mode 100644 index 000000000..04c3780ad --- /dev/null +++ b/backend/src/api/documents/dto/create-document-template.dto.ts @@ -0,0 +1,52 @@ +import { Type } from 'class-transformer'; +import { + IsNotEmpty, + IsNotEmptyObject, + IsObject, + IsString, + MaxLength, + MinLength, + ValidateNested, +} from 'class-validator'; +import { IDocumentTemplateOrganizationData } from 'src/modules/documents/models/document-template.model'; + +class DocumentTemplateOrganizationDataDto + implements IDocumentTemplateOrganizationData +{ + @IsString() + @IsNotEmpty() + officialName: string; + + @IsString() + @IsNotEmpty() + registeredOffice: string; // Sediu social + + @IsString() + @IsNotEmpty() + CUI: string; + + @IsString() + @IsNotEmpty() + legalRepresentativeName: string; + + @IsString() + @IsNotEmpty() + legalRepresentativeRole: string; +} + +export class CreateDocumentTemplateDto { + @IsString() + @MaxLength(250) + @MinLength(2) + name: string; + + @IsObject() + @IsNotEmptyObject() + @ValidateNested() + @Type(() => DocumentTemplateOrganizationDataDto) + organizationData: IDocumentTemplateOrganizationData; + + @IsString() + @IsNotEmpty() + documentTerms: string; +} diff --git a/backend/src/api/documents/presenters/document-template.presenter.ts b/backend/src/api/documents/presenters/document-template.presenter.ts new file mode 100644 index 000000000..07f95181c --- /dev/null +++ b/backend/src/api/documents/presenters/document-template.presenter.ts @@ -0,0 +1,52 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { AdminUserPresenter } from 'src/api/auth/presenters/admin-user.presenter'; +import { + IDocumentTemplateModel, + IDocumentTemplateOrganizationData, +} from 'src/modules/documents/models/document-template.model'; + +export class DocumentTemplatePresenter { + constructor(template: IDocumentTemplateModel) { + this.id = template.id; + this.name = template.name; + this.organizationData = template.organizationData; + this.documentTerms = template.documentTerms; + this.createdByAdmin = template.createdByAdmin + ? new AdminUserPresenter(template.createdByAdmin) + : null; + } + + @Expose() + @ApiProperty({ + description: 'The uuid of the template', + example: '525dcdf9-4117-443e-a0c3-bf652cdc5c1b', + }) + id: string; + + @Expose() + @ApiProperty({ + description: 'The name of the template', + example: 'Template nou', + }) + name: string; + + @Expose() + @ApiProperty({ + description: 'The organization data of the template', + example: + '{ "officialName": "Official name", "registeredOffice": "Registered office (👀 should it be composed?)", "CUI": "RO42211332", "legalRepresentativeName": "Legal representative\'s full name", "legalRepresentativeRole": "Legal representative\'s role"}', + }) + organizationData: IDocumentTemplateOrganizationData; + + @Expose() + @ApiProperty({ + description: 'The document terms of the template', + example: '

Some HTML content

', + }) + documentTerms: string; + + @Expose() + @ApiProperty({ description: 'Admin who created the template' }) + createdByAdmin: AdminUserPresenter; +} diff --git a/backend/src/migrations/1725454854964-AddDocumentTemplate.ts b/backend/src/migrations/1725454854964-AddDocumentTemplate.ts new file mode 100644 index 000000000..c75491a62 --- /dev/null +++ b/backend/src/migrations/1725454854964-AddDocumentTemplate.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Migrations1725454854964 implements MigrationInterface { + name = 'Migrations1725454854964'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "documents_template" ("deleted_on" TIMESTAMP WITH TIME ZONE, "created_on" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_on" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" text NOT NULL, "organization_data" jsonb NOT NULL, "document_terms" text NOT NULL, "organization_id" uuid NOT NULL, "created_by_admin_id" uuid, CONSTRAINT "PK_3c3bacd617c899c37223ebbb037" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_f745fb393b076c4c503437291b" ON "documents_template" ("created_on") `, + ); + await queryRunner.query( + `ALTER TABLE "documents_template" ADD CONSTRAINT "FK_0f3775a66466a4be5293db71421" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "documents_template" ADD CONSTRAINT "FK_b879124283b91c84eb3de436a81" FOREIGN KEY ("created_by_admin_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "documents_template" DROP CONSTRAINT "FK_b879124283b91c84eb3de436a81"`, + ); + await queryRunner.query( + `ALTER TABLE "documents_template" DROP CONSTRAINT "FK_0f3775a66466a4be5293db71421"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_f745fb393b076c4c503437291b"`, + ); + await queryRunner.query(`DROP TABLE "documents_template"`); + } +} diff --git a/backend/src/modules/documents/documents.module.ts b/backend/src/modules/documents/documents.module.ts index ca65a1f0a..fa8bb2a2b 100644 --- a/backend/src/modules/documents/documents.module.ts +++ b/backend/src/modules/documents/documents.module.ts @@ -7,16 +7,27 @@ import { ContractEntity } from './entities/contract.entity'; import { ContractRepositoryService } from './repositories/contract.repository'; import { ContractFacade } from './services/contract.facade'; import { PDFGenerator } from './services/pdf-generator'; +import { DocumentTemplateRepositoryService } from './repositories/document-template.repository'; +import { DocumentTemplateFacade } from './services/document-template.facade'; +import { DocumentTemplateEntity } from './entities/document-template.entity'; @Module({ - imports: [TypeOrmModule.forFeature([TemplateEntity, ContractEntity])], + imports: [ + TypeOrmModule.forFeature([ + TemplateEntity, + ContractEntity, + DocumentTemplateEntity, + ]), + ], providers: [ // Repositories TemplateRepositoryService, ContractRepositoryService, + DocumentTemplateRepositoryService, // Facades TemplateFacade, ContractFacade, + DocumentTemplateFacade, // Services PDFGenerator, ], @@ -25,6 +36,7 @@ import { PDFGenerator } from './services/pdf-generator'; TemplateFacade, ContractFacade, PDFGenerator, + DocumentTemplateFacade, ], }) export class DocumentsModule {} diff --git a/backend/src/modules/documents/entities/document-template.entity.ts b/backend/src/modules/documents/entities/document-template.entity.ts new file mode 100644 index 000000000..179883c39 --- /dev/null +++ b/backend/src/modules/documents/entities/document-template.entity.ts @@ -0,0 +1,51 @@ +import { BaseEntity } from 'src/infrastructure/base/base-entity'; +import { OrganizationEntity } from 'src/modules/organization/entities/organization.entity'; +import { AdminUserEntity } from 'src/modules/user/entities/user.entity'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; + +@Entity({ name: 'documents_template' }) +export class DocumentTemplateEntity extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'text', name: 'name' }) + name: string; + + // { + // "officialName": "Official name", + // "registeredOffice": "Registered office (👀 should it be composed?)", + // "CUI": "RO42211332", + // "legalRepresentativeName": "Legal representative's full name", + // "legalRepresentativeRole": "Legal representative's role" + // } + @Column({ type: 'jsonb', name: 'organization_data' }) + organizationData: object; + + @Column({ type: 'text', name: 'document_terms' }) + documentTerms: string; // HTML string from WYSIWYG + + @Column({ type: 'text', name: 'organization_id' }) + organizationId: string; + + @ManyToOne(() => OrganizationEntity) + @JoinColumn({ name: 'organization_id' }) + organization: OrganizationEntity; + + @Column({ type: 'string', name: 'created_by_admin_id', nullable: true }) + createdByAdminId: string; + + @ManyToOne(() => AdminUserEntity) + @JoinColumn({ name: 'created_by_admin_id' }) + createdByAdmin: AdminUserEntity; + + // @OneToMany(() => ContractEntity, (contract) => contract.template, { + // onDelete: 'SET NULL', + // }) + // contracts: ContractEntity[]; +} diff --git a/backend/src/modules/documents/exceptions/documente-template.exceptions.ts b/backend/src/modules/documents/exceptions/documente-template.exceptions.ts new file mode 100644 index 000000000..28346f6d2 --- /dev/null +++ b/backend/src/modules/documents/exceptions/documente-template.exceptions.ts @@ -0,0 +1,23 @@ +import { BusinessException } from 'src/common/interfaces/business-exception.interface'; + +export enum DocumentTemplateExceptionCodes { + TEMPLATE_001 = 'TEMPLATE_001', + TEMPLATE_002 = 'TEMPLATE_002', +} + +type DocumentTemplateExceptionCodeType = + keyof typeof DocumentTemplateExceptionCodes; + +export const DocumentTemplateExceptionMessages: Record< + DocumentTemplateExceptionCodes, + BusinessException +> = { + [DocumentTemplateExceptionCodes.TEMPLATE_001]: { + code_error: DocumentTemplateExceptionCodes.TEMPLATE_001, + message: 'Not found', + }, + [DocumentTemplateExceptionCodes.TEMPLATE_002]: { + code_error: DocumentTemplateExceptionCodes.TEMPLATE_002, + message: 'Error while creating the document template', + }, +}; diff --git a/backend/src/modules/documents/interfaces/document-template-repository.interface.ts b/backend/src/modules/documents/interfaces/document-template-repository.interface.ts new file mode 100644 index 000000000..81e8f8411 --- /dev/null +++ b/backend/src/modules/documents/interfaces/document-template-repository.interface.ts @@ -0,0 +1,17 @@ +import { IRepositoryWithPagination } from 'src/common/interfaces/repository-with-pagination.interface'; +import { + CreateDocumentTemplateOptions, + FindOneDocumentTemplateOptions, + IDocumentTemplateModel, +} from '../models/document-template.model'; +import { DocumentTemplateEntity } from '../entities/document-template.entity'; + +export interface IDocumentTemplateRepository + extends IRepositoryWithPagination { + create( + newDocumentTemplate: CreateDocumentTemplateOptions, + ): Promise; + findOne( + findOptions: FindOneDocumentTemplateOptions, + ): Promise; +} diff --git a/backend/src/modules/documents/models/document-template.model.ts b/backend/src/modules/documents/models/document-template.model.ts new file mode 100644 index 000000000..628439aac --- /dev/null +++ b/backend/src/modules/documents/models/document-template.model.ts @@ -0,0 +1,66 @@ +import { IBaseModel } from 'src/common/interfaces/base.model'; +import { DocumentTemplateEntity } from '../entities/document-template.entity'; +import { + AdminUserTransformer, + IAdminUserModel, +} from 'src/modules/user/models/admin-user.model'; + +export interface IDocumentTemplateOrganizationData { + officialName: string; + registeredOffice: string; // Sediu social + CUI: string; + legalRepresentativeName: string; + legalRepresentativeRole: string; +} + +export interface IDocumentTemplateModel extends IBaseModel { + id: string; + name: string; + organizationData: IDocumentTemplateOrganizationData; + documentTerms: string; + organizationId: string; + + // Relations + createdByAdmin: IAdminUserModel; +} + +export type CreateDocumentTemplateOptions = Omit< + IDocumentTemplateModel, + 'id' | 'createdOn' | 'updatedOn' | 'createdByAdmin' +> & { createdByAdminId: string }; + +export type FindOneDocumentTemplateOptions = Pick; + +export class DocumentTemplateTransformer { + static fromEntity(entity: DocumentTemplateEntity): IDocumentTemplateModel { + if (!entity) { + return null; + } + + return { + id: entity.id, + name: entity.name, + organizationData: + entity.organizationData as IDocumentTemplateOrganizationData, + documentTerms: entity.documentTerms, + organizationId: entity.organizationId, + createdByAdmin: AdminUserTransformer.fromEntity(entity.createdByAdmin), + createdOn: entity.createdOn, + updatedOn: entity.updatedOn, + }; + } + + static toEntity( + model: CreateDocumentTemplateOptions, + ): DocumentTemplateEntity { + const entity = new DocumentTemplateEntity(); + + entity.name = model.name; + entity.organizationData = model.organizationData; + entity.documentTerms = model.documentTerms; + entity.organizationId = model.organizationId; + entity.createdByAdminId = model.createdByAdminId; + + return entity; + } +} diff --git a/backend/src/modules/documents/repositories/document-template.repository.ts b/backend/src/modules/documents/repositories/document-template.repository.ts new file mode 100644 index 000000000..348ab9b3a --- /dev/null +++ b/backend/src/modules/documents/repositories/document-template.repository.ts @@ -0,0 +1,46 @@ +import { RepositoryWithPagination } from 'src/infrastructure/base/repository-with-pagination.class'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { DocumentTemplateEntity } from '../entities/document-template.entity'; +import { IDocumentTemplateRepository } from '../interfaces/document-template-repository.interface'; +import { + CreateDocumentTemplateOptions, + DocumentTemplateTransformer, + FindOneDocumentTemplateOptions, + IDocumentTemplateModel, +} from '../models/document-template.model'; + +export class DocumentTemplateRepositoryService + extends RepositoryWithPagination + implements IDocumentTemplateRepository +{ + constructor( + @InjectRepository(DocumentTemplateEntity) + private readonly documentTemplateRepository: Repository, + ) { + super(documentTemplateRepository); + } + + async create( + newDocumentTemplate: CreateDocumentTemplateOptions, + ): Promise { + const documentTemplate = await this.documentTemplateRepository.save( + DocumentTemplateTransformer.toEntity(newDocumentTemplate), + ); + + return this.findOne({ id: documentTemplate.id }); + } + + async findOne( + findOptions: FindOneDocumentTemplateOptions, + ): Promise { + const documentTemplate = await this.documentTemplateRepository.findOne({ + where: findOptions, + relations: { + createdByAdmin: true, + }, + }); + + return DocumentTemplateTransformer.fromEntity(documentTemplate); + } +} diff --git a/backend/src/modules/documents/services/document-template.facade.ts b/backend/src/modules/documents/services/document-template.facade.ts new file mode 100644 index 000000000..9e6ed2a49 --- /dev/null +++ b/backend/src/modules/documents/services/document-template.facade.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { DocumentTemplateRepositoryService } from '../repositories/document-template.repository'; +import { + CreateDocumentTemplateOptions, + FindOneDocumentTemplateOptions, + IDocumentTemplateModel, +} from '../models/document-template.model'; + +@Injectable() +export class DocumentTemplateFacade { + constructor( + private readonly documentTemplateRepository: DocumentTemplateRepositoryService, + ) {} + + async create( + newDocumentTemplate: CreateDocumentTemplateOptions, + ): Promise { + return this.documentTemplateRepository.create(newDocumentTemplate); + } + + async findOne( + findOptions: FindOneDocumentTemplateOptions, + ): Promise { + return this.documentTemplateRepository.findOne(findOptions); + } +} diff --git a/backend/src/usecases/documents/create-document-template.usecase.ts b/backend/src/usecases/documents/create-document-template.usecase.ts new file mode 100644 index 000000000..f82a57630 --- /dev/null +++ b/backend/src/usecases/documents/create-document-template.usecase.ts @@ -0,0 +1,39 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { JSONStringifyError } from 'src/common/helpers/utils'; +import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; +import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service'; +import { DocumentTemplateExceptionMessages } from 'src/modules/documents/exceptions/documente-template.exceptions'; +import { + CreateDocumentTemplateOptions, + IDocumentTemplateModel, +} from 'src/modules/documents/models/document-template.model'; +import { DocumentTemplateFacade } from 'src/modules/documents/services/document-template.facade'; + +@Injectable() +export class CreateDocumentTemplateUsecase + implements IUseCaseService +{ + private readonly logger = new Logger(CreateDocumentTemplateUsecase.name); + constructor( + private readonly documentTemplateFacade: DocumentTemplateFacade, + private readonly exceptionService: ExceptionsService, + ) {} + + public async execute( + options: CreateDocumentTemplateOptions, + ): Promise { + try { + const newTemplate = await this.documentTemplateFacade.create(options); + + return newTemplate; + } catch (error) { + this.logger.error({ + ...DocumentTemplateExceptionMessages.TEMPLATE_002, + error: JSONStringifyError(error), + }); + this.exceptionService.badRequestException( + DocumentTemplateExceptionMessages.TEMPLATE_002, + ); + } + } +} diff --git a/backend/src/usecases/documents/get-one-document-template.usecase.ts b/backend/src/usecases/documents/get-one-document-template.usecase.ts new file mode 100644 index 000000000..50143b8f4 --- /dev/null +++ b/backend/src/usecases/documents/get-one-document-template.usecase.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; +import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service'; +import { DocumentTemplateExceptionMessages } from 'src/modules/documents/exceptions/documente-template.exceptions'; +import { + FindOneDocumentTemplateOptions, + IDocumentTemplateModel, +} from 'src/modules/documents/models/document-template.model'; +import { DocumentTemplateFacade } from 'src/modules/documents/services/document-template.facade'; + +@Injectable() +export class GetOneDocumentTemplateUseCase + implements IUseCaseService +{ + constructor( + private readonly documentTemplateFacade: DocumentTemplateFacade, + private readonly exceptionsService: ExceptionsService, + ) {} + + public async execute( + findOptions: FindOneDocumentTemplateOptions, + organizationId: string, + ): Promise { + const template = await this.documentTemplateFacade.findOne(findOptions); + + // Check if the provided organizationId matches the template's organizationId + // If they don't match, throw a forbidden exception + if (!template || organizationId !== template.organizationId) { + this.exceptionsService.notFoundException( + DocumentTemplateExceptionMessages.TEMPLATE_001, + ); + } + + return template; + } +} diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts index a595ac54d..94dde098a 100644 --- a/backend/src/usecases/use-case.module.ts +++ b/backend/src/usecases/use-case.module.ts @@ -138,6 +138,8 @@ import { GetRejectedAccessRequestUsecase } from './access-request/get-rejected-a import { DeleteAccountRegularUserUsecase } from './user/delete-account.usecase'; import { GeneratePDFsUseCase } from './documents/generate-pdfs.usecase'; import { SyncWithOngHubUseCaseService } from './organization/sync-with-ngohub.usecase'; +import { CreateDocumentTemplateUsecase } from './documents/create-document-template.usecase'; +import { GetOneDocumentTemplateUseCase } from './documents/get-one-document-template.usecase'; @Module({ imports: [ @@ -284,6 +286,8 @@ import { SyncWithOngHubUseCaseService } from './organization/sync-with-ngohub.us DeleteTemplateUseCase, GetAllTemplatesUsecase, GetTemplatesForDownloadUsecase, + CreateDocumentTemplateUsecase, + GetOneDocumentTemplateUseCase, // Contracts CreateContractUsecase, GetManyContractsUsecase, @@ -430,6 +434,8 @@ import { SyncWithOngHubUseCaseService } from './organization/sync-with-ngohub.us DeleteTemplateUseCase, GetAllTemplatesUsecase, GetTemplatesForDownloadUsecase, + CreateDocumentTemplateUsecase, + GetOneDocumentTemplateUseCase, // Contracts CreateContractUsecase, GetManyContractsUsecase, From 71977d0ff0d39bdb5ceeb287ed665663bc0a2677 Mon Sep 17 00:00:00 2001 From: luciatugui Date: Thu, 5 Sep 2024 09:20:35 +0300 Subject: [PATCH 14/55] feat: [Contracts] - automatic fill in for all contracts data --- .../src/assets/locales/en/translation.json | 35 +++++- .../src/assets/locales/ro/translation.json | 35 +++++- .../src/components/AutoFillContractCard.tsx | 84 +++++++++++++ frontend/src/components/CardsExample.tsx | 66 +++++++--- frontend/src/components/ContractCard.tsx | 118 +++++++++++++----- .../src/components/ContractCardHeader.tsx | 30 ++++- frontend/src/components/DateRangePicker.tsx | 32 +++-- frontend/src/components/FormInput.tsx | 3 + frontend/src/components/InfoParagraph.tsx | 48 ++++--- frontend/src/components/Input.tsx | 5 +- .../src/components/OrganizationDataError.tsx | 17 ++- frontend/src/components/Signatures.tsx | 32 ++++- .../contracts/ContractTemplatePreview.tsx | 20 ++- .../contracts/OrganizationDataForm.tsx | 35 +++++- .../contracts/OrganizationDetails.tsx | 30 +++++ frontend/src/pages/StepperExample.tsx | 2 +- frontend/src/pages/Volunteers.tsx | 11 +- 17 files changed, 487 insertions(+), 116 deletions(-) create mode 100644 frontend/src/components/AutoFillContractCard.tsx diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json index d99ee6a19..b3e9ab78a 100644 --- a/frontend/src/assets/locales/en/translation.json +++ b/frontend/src/assets/locales/en/translation.json @@ -89,7 +89,8 @@ "no_options": "No options", "type_for_options": "Type for options...", "hide": "Show less", - "show": "Show more" + "show": "Show more", + "loading": "Loading..." }, "header": { "login": "Login", @@ -1012,7 +1013,18 @@ }, "organization_data_form": { "loading_error": "We encountered a problem while loading the organization data", - "retry_button": "Retry" + "synced": { + "is_syncing": "Updating...", + "p1": "Have you updated the data in NGO Hub? Press", + "p2": "here", + "p3": "to synchronise" + }, + "retry_button": "Retry", + "missing_name": "Organization name is missing", + "missing_address": "Organization address is missing", + "missing_cui": "Organization CUI is missing", + "missing_legalReprezentativeFullName": "Organization legal representative's name is missing", + "missing_legalReprezentativeRole": "Organization legal representative's role is missing" } }, "volunteer": { @@ -1023,7 +1035,8 @@ "series": "[Series]", "no": "[Number]", "institution": "[Issuing institution]", - "eliberation_date": "[Issuance date]" + "eliberation_date": "[Issuance date]", + "missing_data": "There is missing data for generating the contract for this volunteer." }, "template_preview": { "title": "VOLUNTEERING CONTRACT", @@ -1079,6 +1092,8 @@ "title": "Edit text" }, "organization_name": "Organization name", + "organization_address": "Organization address", + "organization_cui": "CUI", "represented_by": "Represented by", "legal_representative": "Legal representative of the minor", "identification": "ID series and number", @@ -1086,6 +1101,7 @@ "tel_no": "Phone number", "volunteer_name": "Volunteer name", "legal_rep_name": "Legal representative's name", + "legal_rep_role": "Legal representative's role", "number": "Number", "telephone": "Phone" }, @@ -1095,5 +1111,16 @@ "fill": "Fill", "attachments": "Attachments", "complete": "Complete" + }, + "fast_contract_fill": { + "title": "Quick Fill", + "description": "Pre-fill the data below with a single click! You can modify the data individually later.", + "form": { + "starting_number": "Consecutive contract numbers starting with", + "contract_date": "Contract date", + "contract_period": "Contract period" + }, + "clear": "Clear", + "apply_all": "Apply to all" } -} +} \ No newline at end of file diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 846ca48ed..84577aff5 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -89,7 +89,8 @@ "no_options": "Fără opțiuni", "type_for_options": "Începeți să scrieți pentru opțiuni...", "hide": "Vezi mai puțin", - "show": "Vezi tot" + "show": "Vezi tot", + "loading": "Se încarcă..." }, "header": { "login": "Login", @@ -1012,7 +1013,18 @@ }, "organization_data_form": { "loading_error": "Am întâmpinat o problemă la încărcarea datelor despre organizație", - "retry_button": "Reîncearcă" + "synced": { + "is_syncing": "Se actualizează...", + "p1": "Ai actualizat datele în NGO Hub? Apasă", + "p2": "aici", + "p3": "pentru sincronizare" + }, + "retry_button": "Reîncearcă", + "missing_name": "Numele organizației lipsește", + "missing_address": "Adresa organizației lipsește", + "missing_cui": "CUI-ul organizației lipsește", + "missing_legalReprezentativeFullName": "Numele reprezentantului legal al organizației lipsește", + "missing_legalReprezentativeRole": "Rolul reprezentantului legal al organizației lipsește" } }, "volunteer": { @@ -1023,7 +1035,8 @@ "series": "[Serie]", "no": "[Număr]", "institution": "[Unitate eliberare]", - "eliberation_date": "[Data eliberării]" + "eliberation_date": "[Data eliberării]", + "missing_data": "Există date lipsă la generarea contractului pentru acest voluntar." }, "template_preview": { "title": "CONTRACT DE VOLUNTARIAT", @@ -1079,6 +1092,8 @@ "title": "Editează text" }, "organization_name": "Nume organizație", + "organization_address": "Sediu organizație", + "organization_cui": "CUI", "represented_by": "Reprezentată de", "legal_representative": "Reprezentantul legal al minorului", "identification": "Serie si nr. CI", @@ -1086,6 +1101,7 @@ "tel_no": "Nr. Tel.", "volunteer_name": "Nume voluntar", "legal_rep_name": "Nume reprezentant legal", + "legal_rep_role": "Rolul reprezentantului legal", "number": "Număr", "telephone": "Telefon" }, @@ -1095,5 +1111,16 @@ "fill": "Completează", "attachments": "Anexe", "complete": "Finalizează" + }, + "fast_contract_fill": { + "title": "Completare rapidă", + "description": "Precompletează datele de mai jos dintr-un singur click! Poți modifica datele individual ulterior.", + "form": { + "starting_number": "Numere contract consecutive începând cu", + "contract_date": "Data contractului", + "contract_period": "Perioadă contract" + }, + "clear": "Șterge", + "apply_all": "Aplică pentru toate" } -} +} \ No newline at end of file diff --git a/frontend/src/components/AutoFillContractCard.tsx b/frontend/src/components/AutoFillContractCard.tsx new file mode 100644 index 000000000..4424a60eb --- /dev/null +++ b/frontend/src/components/AutoFillContractCard.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Control, Controller, FieldValues, UseFormHandleSubmit } from 'react-hook-form'; +import FormInput from './FormInput'; +import FormDatePicker from './FormDatePicker'; +import DateRangePicker from './DateRangePicker'; +import { useTranslation } from 'react-i18next'; +import Button from './Button'; + +interface AutoFillContractCardProps { + control: Control; + handleReset: () => void; + handleSubmit: UseFormHandleSubmit; + onSubmit: ({ startingNumber, contractDate, contractPeriod }: FieldValues) => void; +} + +export const AutoFillContractCard = ({ + control, + handleReset, + handleSubmit, + onSubmit, +}: AutoFillContractCardProps) => { + const { t } = useTranslation('fast_contract_fill'); + + return ( + <> +
+

{t('title')}

+

{t('description')}

+
+ ( + + )} + /> + ( + + )} + /> + + ( + + )} + /> +
+
+
+
+ + ); +}; diff --git a/frontend/src/components/CardsExample.tsx b/frontend/src/components/CardsExample.tsx index a5eb5c4bd..f61d235dd 100644 --- a/frontend/src/components/CardsExample.tsx +++ b/frontend/src/components/CardsExample.tsx @@ -1,8 +1,7 @@ -import React from 'react'; +import React, { useState } from 'react'; import { ContractCard } from './ContractCard'; -import FormInput from './FormInput'; -import FormDatePicker from './FormDatePicker'; -import DateRangePicker from './DateRangePicker'; +import { FieldValues, useForm } from 'react-hook-form'; +import { AutoFillContractCard } from './AutoFillContractCard'; const items = [ { @@ -110,24 +109,53 @@ const items = [ ]; export const CardsExample = () => { + const { control, reset, handleSubmit } = useForm(); + + const [startingNumber, setStartingNumber] = useState(''); + const [contractDate, setContractDate] = useState(null); + const [contractPeriod, setContractPeriod] = useState<[Date | null, Date | null]>([null, null]); + + const handleReset = () => { + setStartingNumber(''); + setContractDate(null); + setContractPeriod([null, null]); + reset({ + startingNumber: '', + contractDate: null, + contractPeriod: [null, null], + }); + }; + + const onSubmit = ({ startingNumber, contractDate, contractPeriod }: FieldValues) => { + if (startingNumber) { + setStartingNumber(startingNumber); + } + if (contractDate) { + setContractDate(contractDate); + } + if (contractPeriod) { + setContractPeriod(contractPeriod); + } + }; + return ( <> -
-

Completare rapidă

-

- Precompletează datele de mai jos dintr-un singur click! Poți modifica datele individual - ulterior. -

-
- - - {/* //todo: something about this */} - -
-
+ +
- {items.map((item) => ( - + {items.map((item, index) => ( + ))}
diff --git a/frontend/src/components/ContractCard.tsx b/frontend/src/components/ContractCard.tsx index ccd3bcdf1..b8df5b22d 100644 --- a/frontend/src/components/ContractCard.tsx +++ b/frontend/src/components/ContractCard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import FormInput from './FormInput'; import Button from './Button'; import { Controller, FieldValues, useForm } from 'react-hook-form'; @@ -34,30 +34,74 @@ export interface IMockVolunteer { interface ContractCardProps { data: { contract: IMockContract; volunteer: IMockVolunteer }; + initialNumber?: string; + initialDate?: Date | null; + initialPeriod?: [Date | null, Date | null]; } const dotsString = '.........................'; const contractTerms = '

h1

h2

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quis lobortis nisl cursus bibendum sit nulla accumsan sodales ornare. At urna viverra non suspendisse neque, lorem. Pretium condimentum pellentesque gravida id etiam sit sed arcu euismod. Rhoncus proin orci duis scelerisque molestie cursus tincidunt aliquam.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quis lobortis nisl cursus bibendum sit nulla accumsan sodales ornare. At urna viverra non suspendisse neque, lorem. Pretium condimentum pellentesque gravida id etiam sit sed arcu euismod. Rhoncus proin orci duis scelerisque molestie cursus tincidunt aliquam.

'; -export const ContractCard = ({ data }: ContractCardProps) => { +export const ContractCard = ({ + data, + initialNumber, + initialDate, + initialPeriod, +}: ContractCardProps) => { const { t } = useTranslation(['doc_templates', 'general']); - //? todo: get the contract data from a query and not from props - const { contract, volunteer } = data; - // todo: delete this - console.log(contract); - // contract card states const [open, setOpen] = useState(false); const [edit, setEdit] = useState(false); - const [contractNumber, setContractNumber] = useState(dotsString); - const [contractDate, setContractDate] = useState(dotsString); - const [contractPeriod, setContractPeriod] = useState([dotsString, dotsString]); - - const { data: organization } = useOrganizationQuery(); - const { control, handleSubmit, setValue } = useForm(); + // queries + const { data: organization, isLoading: isLoadingOrganization } = useOrganizationQuery(); + //? todo: get the contract data from a query and not from props + const { contract, volunteer } = data; + console.log(contract); + const isVolunteerDataIncomplete = true; + const { control, handleSubmit, setValue } = useForm({ + defaultValues: { + contractNumber: initialNumber || '', + contractDate: initialDate || null, + contractPeriod: initialPeriod || [null, null], + }, + }); + + // update values for the contract data, as well as for the contract preview, whenever the initial values coming from the parent change (the fast contract completion feature) + useEffect(() => { + // update contract number + setContractNumber(initialNumber ? initialNumber : dotsString); + setValue('contractNumber', initialNumber ? initialNumber : ''); + + // update contract date + setContractDate(initialDate ? initialDate.toLocaleDateString() : dotsString); + setValue('contractDate', initialDate ? initialDate : null); + + // update contract period + setContractPeriod([ + initialPeriod && initialPeriod[0] ? initialPeriod[0].toLocaleDateString() : dotsString, + initialPeriod && initialPeriod[1] ? initialPeriod[1].toLocaleDateString() : dotsString, + ]); + setValue( + 'contractPeriod', + initialPeriod && initialPeriod[0] && initialPeriod[1] + ? [initialPeriod[0], initialPeriod[1]] + : [null, null], + ); + }, [initialNumber, initialDate, initialPeriod]); + + const [contractNumber, setContractNumber] = useState(initialNumber ? initialNumber : dotsString); + const [contractDate, setContractDate] = useState( + initialDate ? initialDate.toLocaleDateString() : dotsString, + ); + const [contractPeriod, setContractPeriod] = useState( + initialPeriod && initialPeriod[0] && initialPeriod[1] + ? [initialPeriod[0].toLocaleDateString(), initialPeriod[1].toLocaleDateString()] + : [dotsString, dotsString], + ); + // on submit -> update the values in the contract preview const onSubmit = (data: FieldValues) => { if (data.contractNumber) { setContractNumber(data.contractNumber); @@ -67,7 +111,7 @@ export const ContractCard = ({ data }: ContractCardProps) => { setContractDate(data.contractDate.toLocaleDateString()); } - if (data.contractPeriod) { + if (data.contractPeriod && data.contractPeriod[0] && data.contractPeriod[1]) { setContractPeriod([ data.contractPeriod[0].toLocaleDateString(), data.contractPeriod[1].toLocaleDateString(), @@ -79,7 +123,14 @@ export const ContractCard = ({ data }: ContractCardProps) => { return (
- + {open && (
@@ -90,9 +141,8 @@ export const ContractCard = ({ data }: ContractCardProps) => { ( + render={({ field: { value = initialNumber, onChange } }) => ( { )} /> - {/* no controller here, as the value of this component is managed differently inside */} - setValue('contractPeriod', value)} + ( + + )} /> - {/* //? ce facem dupa ce editam astea? */}
); diff --git a/frontend/src/components/Signatures.tsx b/frontend/src/components/Signatures.tsx index 2511f0cc3..095c7b7bb 100644 --- a/frontend/src/components/Signatures.tsx +++ b/frontend/src/components/Signatures.tsx @@ -4,6 +4,8 @@ import { Signature } from './Signature'; import { useTranslation } from 'react-i18next'; import { useOrganizationQuery } from '../services/organization/organization.service'; import { IMockVolunteer } from './ContractCard'; +import LoadingContent from './LoadingContent'; +import { OrganizationDataError } from './OrganizationDataError'; interface SignatureProps { volunteer?: IMockVolunteer; @@ -11,7 +13,23 @@ interface SignatureProps { export const Signatures = ({ volunteer }: SignatureProps) => { const { t } = useTranslation('doc_templates'); - const { data: organization } = useOrganizationQuery(); + const { + data: organization, + isLoading: isLoadingOrganization, + isError: isErrorOrganization, + isFetching, + refetch, + } = useOrganizationQuery(); + + if (isLoadingOrganization) { + return ; + } + + if (isErrorOrganization) { + return ; + } + + const isOrganizationNameMissing = !organization?.name; return (
@@ -23,7 +41,9 @@ export const Signatures = ({ volunteer }: SignatureProps) => { {volunteer ? (

{organization?.name}

) : ( - + )}
} @@ -31,7 +51,13 @@ export const Signatures = ({ volunteer }: SignatureProps) => {
{' '} {t('represented_by')} - {volunteer ?

{organization?.legalRepresentative}

: } + {volunteer ? ( +

{organization?.legalReprezentativeFullName}

+ ) : ( + + )}
} /> diff --git a/frontend/src/components/contracts/ContractTemplatePreview.tsx b/frontend/src/components/contracts/ContractTemplatePreview.tsx index 8ce068c71..36b5366ee 100644 --- a/frontend/src/components/contracts/ContractTemplatePreview.tsx +++ b/frontend/src/components/contracts/ContractTemplatePreview.tsx @@ -16,6 +16,7 @@ export const ContractTemplatePreview = () => { isLoading: isLoadingOrganizationData, isError: isErrorOrganizationData, refetch: refetchOrganizationData, + isFetching, } = useOrganizationQuery(); const handleRefetch = () => { @@ -40,7 +41,7 @@ export const ContractTemplatePreview = () => { if (isErrorOrganizationData) return (
- +
); @@ -72,21 +73,28 @@ export const ContractTemplatePreview = () => { {/* P1 */}

{t('template_preview.p2.between')}{' '} - + {', '} - {t('template_preview.p2.address')} + {t('template_preview.p2.address')}{' '} + {', '} {t('template_preview.p2.identified')}{' '} - + {', '} {t('template_preview.p2.represented_by')}{' '} {', '} {t('template_preview.p2.as')}{' '} - {' '} + {' '} {t('template_preview.p2.named')}{' '} {t('template_preview.p2.organization')}{' '}

diff --git a/frontend/src/components/contracts/OrganizationDataForm.tsx b/frontend/src/components/contracts/OrganizationDataForm.tsx index 904659bf9..c9875187f 100644 --- a/frontend/src/components/contracts/OrganizationDataForm.tsx +++ b/frontend/src/components/contracts/OrganizationDataForm.tsx @@ -8,14 +8,14 @@ import { OrganizationDataError } from '../OrganizationDataError'; export const OrganizationDataForm = () => { const { t } = useTranslation('doc_templates'); - const { data: organization, isLoading, isError, refetch } = useOrganizationQuery(); + const { data: organization, isLoading, isError, refetch, isFetching } = useOrganizationQuery(); const handleRefetch = () => { refetch(); }; if (isLoading) return ; - if (isError) return ; + if (isError) return ; return ( <> @@ -23,8 +23,13 @@ export const OrganizationDataForm = () => { type="text" disabled label={t('organization.name')} - value={organization ? organization.name : t('organization_name')} + value={organization ? organization.name : ''} id="organization_name" + errorMessage={ + organization && !organization.name + ? t('organization.organization_data_form.missing_name') + : undefined + } /> { label={t('organization.address')} value={organization?.address || ''} id="organization_address" + errorMessage={ + organization && !organization.address + ? t('organization.organization_data_form.missing_address') + : undefined + } /> { label={t('organization.cui')} value={organization?.cui || ''} id="organization_CUI" + errorMessage={ + organization && !organization.cui + ? t('organization.organization_data_form.missing_cui') + : undefined + } /> ); diff --git a/frontend/src/components/contracts/OrganizationDetails.tsx b/frontend/src/components/contracts/OrganizationDetails.tsx index 06f551530..31565278f 100644 --- a/frontend/src/components/contracts/OrganizationDetails.tsx +++ b/frontend/src/components/contracts/OrganizationDetails.tsx @@ -3,11 +3,21 @@ import FormInput from '../FormInput'; import { useTranslation } from 'react-i18next'; import { OrganizationDataForm } from './OrganizationDataForm'; import { Controller, useForm } from 'react-hook-form'; +import { useResyncOrganizationWithOngHubMutation } from '../../services/organization/organization.service'; +import Button from '../Button'; +import { useQueryClient } from 'react-query'; export const OrganizationDetails = () => { const { t } = useTranslation('doc_templates'); const { control } = useForm(); + const queryClient = useQueryClient(); + + const { + mutate: resyncOrganizationWithOngHub, + isLoading: isResyncingOrganizationWithOngHubLoading, + } = useResyncOrganizationWithOngHubMutation(); + return (
{/* //TODO: functionality to save the contract template name */} @@ -29,6 +39,26 @@ export const OrganizationDetails = () => { ). {t('organization.edit')}

+ {isResyncingOrganizationWithOngHubLoading ? ( + t('organization.organization_data_form.synced.is_syncing') + ) : ( +

+ {t('organization.organization_data_form.synced.p1') + ' '} +