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
+
+
+
+ Ziua
+ Ora
+ Activitate
+ Locație
+ Priorități
+
+
+
+
+ Luni
+ 09:00 - 12:00
+ Asistență administrativă
+ Biroul central
+ Medie
+
+
+ 14:00 - 17:00
+ Organizare evenimente
+ Sala de conferințe
+ Înaltă
+
+
+ Marți
+ 10:00 - 15:00
+ Activități cu beneficiarii
+ Centrul comunitar
+ Înaltă
+
+
+ Miercuri
+ 09:00 - 11:00
+ Întâlnire echipă
+ Biroul central
+ Medie
+
+
+ 12:00 - 16:00
+ Strângere de fonduri
+ Diverse locații
+ Înaltă
+
+
+ Joi
+ 13:00 - 18:00
+ Mentorat tineri
+ Școala locală
+ Medie
+
+
+ Vineri
+ 10:00 - 14:00
+ Activități ecologice
+ Parcul orașului
+ Scă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}
+
+
+
+
+
+ );
+};
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')}
+
+ }
+ className="btn-outline-secondary text-cool-gray-600 "
+ // TODO: descarca necompletat functionality
+ onClick={() => {}}
+ />
+ {/* // TODO: save functionality */}
+
+
+
+
+
+ {/* 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 ;
+ }}
+ />
+
+ {
+ // reset the internal value of the RichTextEditor to that before modifications
+ reset({ contractTerms });
+ // close editor
+ setEditingText(false);
+ }}
+ />
+
+
+
+ );
+ }
+
+ 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')}
+
}
+ onClick={() => setEditingText(true)}
+ />
+
+
+
+ );
+};
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')}
+
+
+
+ );
+};
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')}
- }
- onClick={onEditButtonClick}
- />
+
+
+ resyncOrganizationWithOngHub(undefined, {
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['organization'] });
+ },
+ })
+ }
+ />
+ }
+ onClick={onEditButtonClick}
+ />
+
@@ -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? */}
+ setEdit(true)}
+ />
+
+
+ {/* 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 &&
{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? */}
{
{t('template_preview.p2.between')}{' '}
- {organization?.name || ''} {' '}
- {t('template_preview.p2.address')} {organization?.address || ''}{' '}
+
+ {organization?.name || `[${t('organization_name')}]`}
+ {' '}
+ {t('template_preview.p2.address')}{' '}
+ {organization?.address || `[${t('organization_address')}]`}{' '}
{t('template_preview.p2.identified')}
- {organization?.cui || '!!!!'}
+
+ {' '}
+ {organization?.cui || `[${t('organization_cui')}]`}
+
{', '}
{t('template_preview.p2.represented_by')}{' '}
{' '}
- {organization?.legalRepresentative || '!!!!!!'}
+ {organization?.legalReprezentativeFullName || `[${t('legal_rep_name')}]`}
{', '}
- {t('template_preview.p2.as')} {organization?.legalRepresentativeRole || '!!!!!!!!!'}{' '}
+ {t('template_preview.p2.as')}{' '}
+ {organization?.legalReprezentativeRole || `[${t('legal_rep_role')}]`}{' '}
{t('template_preview.p2.named')}{' '}
{t('template_preview.p2.organization')} {' '}
diff --git a/frontend/src/components/ContractCardHeader.tsx b/frontend/src/components/ContractCardHeader.tsx
index 5d939ff50..b71d9a3b6 100644
--- a/frontend/src/components/ContractCardHeader.tsx
+++ b/frontend/src/components/ContractCardHeader.tsx
@@ -3,17 +3,39 @@ import { TrashIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/s
import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
import { IMockVolunteer } from './ContractCard';
import { Tooltip } from 'react-tooltip';
+import LoadingContent from './LoadingContent';
interface ContractCardHeaderProps {
open: boolean;
setOpen: React.Dispatch>;
volunteer: IMockVolunteer;
// todo: onDelete
+ isLoading?: boolean;
+ isError?: boolean;
+ isErrorText?: string;
}
-export const ContractCardHeader = ({ open, setOpen, volunteer }: ContractCardHeaderProps) => {
+export const ContractCardHeader = ({
+ open,
+ setOpen,
+ volunteer,
+ isLoading,
+ isError,
+ isErrorText,
+}: ContractCardHeaderProps) => {
// todo: remove isError from here and use the right thing instead
- const isError = true;
+ // todo: error state
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
return (
@@ -44,9 +66,7 @@ export const ContractCardHeader = ({ open, setOpen, volunteer }: ContractCardHea
void;
+ value?: [Date | null, Date | null];
+ onChange?: (range: [Date | null, Date | null]) => void;
disabled?: boolean;
className?: string;
}
@@ -22,32 +22,28 @@ const DateRangePicker = ({
disabled,
className,
}: DateRangePickerProps) => {
- const [dateRange, setDateRange] = useState(value || []);
- const [startDate, endDate] = dateRange;
+ const [dateRange, setDateRange] = useState<[Date | null, Date | null]>(value || [null, null]);
useEffect(() => {
- if (value) {
+ // Update internal state when value prop changes, including when it's reset to undefined
+ if (value === undefined) {
+ setDateRange([null, null]);
+ } else if (value && (value[0] !== dateRange[0] || value[1] !== dateRange[1])) {
setDateRange(value);
- } else {
- setDateRange([]);
}
}, [value]);
- useEffect(() => {
- if (dateRange[0] && dateRange[1]) {
- onChange && onChange(dateRange);
- }
- }, [dateRange]);
-
- const onChangeDate = (update: Date[] | unknown) => {
- setDateRange(update as Date[]);
+ const onChangeDate = (update: [Date | null, Date | null] | unknown) => {
+ const newDateRange = update as [Date | null, Date | null];
+ setDateRange(newDateRange);
+ onChange && onChange(newDateRange);
};
return (
{label &&
{label} }
-
+
{
@@ -31,6 +33,7 @@ const FormInput = ({
: '',
className || '',
)}
+ wrapperClassname={wrapperClassname}
aria-invalid={errorMessage ? 'true' : 'false'}
{...props}
helper={errorMessage ? {errorMessage}
: helper}
diff --git a/frontend/src/components/InfoParagraph.tsx b/frontend/src/components/InfoParagraph.tsx
index caed77b4e..5ab75a400 100644
--- a/frontend/src/components/InfoParagraph.tsx
+++ b/frontend/src/components/InfoParagraph.tsx
@@ -6,6 +6,7 @@ interface InfoParagraphProps
extends DetailedHTMLProps, HTMLParagraphElement> {
text: string;
tooltip?: boolean;
+ tooltipTheme?: 'info' | 'error';
tooltipContent?: string;
highlighted?: boolean;
}
@@ -13,6 +14,7 @@ interface InfoParagraphProps
export const InfoParagraph = ({
text,
tooltip,
+ tooltipTheme = 'info',
tooltipContent,
highlighted,
className,
@@ -23,26 +25,42 @@ export const InfoParagraph = ({
return (
<>
{text}
- {tooltip && (
-
- )}
+ {tooltip &&
+ (tooltipTheme === 'info' ? (
+
+ ) : (
+
+ ))}
>
);
};
diff --git a/frontend/src/components/Input.tsx b/frontend/src/components/Input.tsx
index 8c9132d6c..b952fd18f 100644
--- a/frontend/src/components/Input.tsx
+++ b/frontend/src/components/Input.tsx
@@ -3,11 +3,12 @@ import React, { ComponentPropsWithoutRef, ReactNode } from 'react';
export interface InputProps extends ComponentPropsWithoutRef<'input'> {
label?: string;
helper?: ReactNode;
+ wrapperClassname?: string;
}
-const Input = ({ label, helper, ...props }: InputProps) => {
+const Input = ({ label, helper, wrapperClassname, ...props }: InputProps) => {
return (
-
+
{label &&
{label} }
{helper}
diff --git a/frontend/src/components/OrganizationDataError.tsx b/frontend/src/components/OrganizationDataError.tsx
index 45f613fef..8ad19d1c7 100644
--- a/frontend/src/components/OrganizationDataError.tsx
+++ b/frontend/src/components/OrganizationDataError.tsx
@@ -3,8 +3,14 @@ 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');
+export const OrganizationDataError = ({
+ onRetry,
+ isFetching,
+}: {
+ onRetry: () => void;
+ isFetching: boolean;
+}) => {
+ const { t } = useTranslation(['doc_templates', 'general']);
return (
@@ -13,9 +19,14 @@ export const OrganizationDataError = ({ onRetry }: { onRetry: () => void }) => {
{t('organization.organization_data_form.loading_error')}
);
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') + ' '}
+
+ resyncOrganizationWithOngHub(undefined, {
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['organization'] });
+ },
+ })
+ }
+ />
+ {' ' + t('organization.organization_data_form.synced.p3')}
+
+ )}
+
diff --git a/frontend/src/pages/StepperExample.tsx b/frontend/src/pages/StepperExample.tsx
index 38e67f09d..f2fb2fc6a 100644
--- a/frontend/src/pages/StepperExample.tsx
+++ b/frontend/src/pages/StepperExample.tsx
@@ -46,7 +46,7 @@ export const StepperExample = () => {
};
return (
-
+
Stepper Example
{
role: undefined,
age: undefined,
createdOnEnd: undefined,
- createdOnStart: undefined
- }, 'replaceIn');
+ createdOnStart: undefined,
+ },
+ 'replaceIn',
+ );
};
const onExport = async () => {
@@ -477,7 +479,10 @@ const Volunteers = ({ query, setQuery }: VolunteersProps) => {
>
{
+ const [createdOnStart, createdOnEnd] = range;
+ onCreatedOnRangeChange([createdOnStart as Date, createdOnEnd as Date]);
+ }}
value={
query?.createdOnStart && query?.createdOnEnd
? [query?.createdOnStart, query?.createdOnEnd]
From 864e834e1d20f236c93294efa850184da482b1e8 Mon Sep 17 00:00:00 2001
From: luciatugui
Date: Fri, 6 Sep 2024 09:43:15 +0300
Subject: [PATCH 15/55] wip: [Contracts] - Contracts and templates list
---
.../src/assets/locales/en/translation.json | 23 +
.../src/assets/locales/ro/translation.json | 25 +-
frontend/src/common/constants/routes.ts | 12 +-
.../src/components/ContractsStatistics.tsx | 52 ++
.../src/components/DataTableComponent.tsx | 11 +
frontend/src/components/NewContractsTable.tsx | 565 ++++++++++++++++++
frontend/src/components/NewTemplatesTable.tsx | 208 +++++++
.../components/RichText/RichTextEditor.tsx | 9 +-
frontend/src/components/StatisticsCard.tsx | 5 +-
.../contracts/ContractTemplatePreview.tsx | 20 +-
.../components/contracts/ContractTerms.tsx | 45 +-
.../contracts/ContractTermsEmptyState.tsx | 8 +-
.../contracts/OrganizationDetails.tsx | 24 +-
.../NewContractsTableWithQueryParams.tsx | 56 ++
.../NewTemplatesTableWithQueryParams.tsx | 25 +
frontend/src/index.css | 12 +
frontend/src/pages/ContractTemplates.tsx | 180 ++++--
frontend/src/pages/CreateContractTemplate.tsx | 168 ++++++
frontend/src/pages/GenerateContract.tsx | 212 +++++++
frontend/src/routes/Router.tsx | 9 +-
.../documents-templates.api.ts | 10 +
.../documents-templates.service.ts | 38 ++
22 files changed, 1622 insertions(+), 95 deletions(-)
create mode 100644 frontend/src/components/ContractsStatistics.tsx
create mode 100644 frontend/src/components/NewContractsTable.tsx
create mode 100644 frontend/src/components/NewTemplatesTable.tsx
create mode 100644 frontend/src/containers/query/NewContractsTableWithQueryParams.tsx
create mode 100644 frontend/src/containers/query/NewTemplatesTableWithQueryParams.tsx
create mode 100644 frontend/src/pages/CreateContractTemplate.tsx
create mode 100644 frontend/src/pages/GenerateContract.tsx
create mode 100644 frontend/src/services/documents-templates/documents-templates.api.ts
create mode 100644 frontend/src/services/documents-templates/documents-templates.service.ts
diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json
index b3e9ab78a..7444b21e2 100644
--- a/frontend/src/assets/locales/en/translation.json
+++ b/frontend/src/assets/locales/en/translation.json
@@ -978,6 +978,29 @@
"confirm": "Confirm and sign"
}
},
+ "volunteering_contracts": {
+ "title": "Volunteering contracts",
+ "templates": "Contract templates",
+ "description": "View the list of all your volunteer contracts in VIC. The volunteer contract has a predefined structure. You can create multiple contract templates (e.g. contract for volunteers >16 years old, contract for volunteers <16 years old).",
+ "tabs": {
+ "contracts": "Contract list",
+ "templates": "Templates"
+ },
+ "statistics": {
+ "active_contracts": "Active contracts",
+ "in_signing_contracts": "Contracts in signing process",
+ "saved_contracts": "Saved contracts (unsent)",
+ "to_expire_soon": "Contracts expiring soon"
+ },
+ "table_header": {
+ "title": "Contract templates",
+ "download_all": "Download all",
+ "create_template": "Create template"
+ },
+ "generate": {
+ "title": "Generate contract"
+ }
+ },
"doc_templates": {
"title": "Create template",
"subheading": {
diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json
index 84577aff5..69c11f4c6 100644
--- a/frontend/src/assets/locales/ro/translation.json
+++ b/frontend/src/assets/locales/ro/translation.json
@@ -978,6 +978,29 @@
"confirm": "Confirmă și semnează"
}
},
+ "volunteering_contracts": {
+ "title": "Contracte de voluntariat",
+ "templates": "Template-uri de contracte",
+ "description": "Vizualizează lista tuturor contractelor voluntarilor tăi din VIC. Contractul de voluntar are o structura prestabilită. Poți crea mai multe template-uri de contracte (ex. contract pentru voluntari >16 ani, contract pentru voluntari <16 ani).",
+ "tabs": {
+ "contracts": "Listă contracte",
+ "templates": "Template-uri"
+ },
+ "statistics": {
+ "active_contracts": "Contracte active",
+ "in_signing_contracts": "Contracte în curs de semnare",
+ "saved_contracts": "Contracte salvate (netrimise)",
+ "to_expire_soon": "Contracte care expiră curând"
+ },
+ "table_header": {
+ "title": "Template-uri de contracte",
+ "download_all": "Descarcă toate",
+ "create_template": "Creează template"
+ },
+ "generate": {
+ "title": "Generează contract"
+ }
+ },
"doc_templates": {
"title": "Creează template",
"subheading": {
@@ -1009,7 +1032,7 @@
},
"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."
+ "description": " Voluntarii sub 16 ani vor trebui să 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",
diff --git a/frontend/src/common/constants/routes.ts b/frontend/src/common/constants/routes.ts
index 0fbcdd292..3dffefb30 100644
--- a/frontend/src/common/constants/routes.ts
+++ b/frontend/src/common/constants/routes.ts
@@ -68,11 +68,21 @@ export const ROUTES: IRoute[] = [
},
{
id: 62,
- name: i18n.t('general:templates'),
+ name: 'Contracte * NEW *',
href: 'documents/templates',
},
{
id: 63,
+ name: 'Creează template * NEW *',
+ href: 'documents/templates/create',
+ },
+ {
+ id: 64,
+ name: 'Generează contract * NEW *',
+ href: 'documents/templates/contracts/generate',
+ },
+ {
+ id: 65,
name: 'stepper',
href: 'documents/templates/stepper_example',
},
diff --git a/frontend/src/components/ContractsStatistics.tsx b/frontend/src/components/ContractsStatistics.tsx
new file mode 100644
index 000000000..7e930e2ac
--- /dev/null
+++ b/frontend/src/components/ContractsStatistics.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import StatisticsCard from './StatisticsCard';
+import { useTranslation } from 'react-i18next';
+
+export const ContractsStatistics = () => {
+ const { t } = useTranslation('volunteering_contracts');
+
+ return (
+
+ {},
+ }}
+ />
+ {},
+ }}
+ />
+ {},
+ }}
+ />
+ {},
+ }}
+ />
+
+ );
+};
diff --git a/frontend/src/components/DataTableComponent.tsx b/frontend/src/components/DataTableComponent.tsx
index 6d2cde4d2..f9e3f257d 100644
--- a/frontend/src/components/DataTableComponent.tsx
+++ b/frontend/src/components/DataTableComponent.tsx
@@ -18,6 +18,9 @@ interface DataTableProps {
paginationDefaultPage?: number;
onChangePage?: (page: number) => void;
onChangeRowsPerPage?: (rowsPerPage: number) => void;
+ selectableRows?: boolean;
+ selectableRowsSingle?: boolean;
+ onSelectedRowsChange?: (selectedRows: T[]) => void;
onSort?: (selectedColumn: TableColumn, sortDirection: SortOrder, sortedRows: T[]) => void;
defaultSortFieldId?: string | number;
defaultSortAsc?: boolean;
@@ -35,6 +38,9 @@ const DataTableComponent = ({
onSort,
onChangePage,
onChangeRowsPerPage,
+ selectableRows,
+ selectableRowsSingle,
+ onSelectedRowsChange,
defaultSortFieldId,
defaultSortAsc,
}: DataTableProps) => {
@@ -68,6 +74,11 @@ const DataTableComponent = ({
progressComponent={ }
defaultSortFieldId={defaultSortFieldId}
defaultSortAsc={defaultSortAsc}
+ selectableRows={selectableRows}
+ selectableRowsSingle={selectableRowsSingle}
+ onSelectedRowsChange={(selected) =>
+ onSelectedRowsChange && onSelectedRowsChange(selected.selectedRows)
+ }
/>
);
};
diff --git a/frontend/src/components/NewContractsTable.tsx b/frontend/src/components/NewContractsTable.tsx
new file mode 100644
index 000000000..1f7c8181c
--- /dev/null
+++ b/frontend/src/components/NewContractsTable.tsx
@@ -0,0 +1,565 @@
+import React, { useEffect, useState } from 'react';
+import DataTableComponent from './DataTableComponent';
+import CardHeader from './CardHeader';
+import CardBody from './CardBody';
+import Card from '../layouts/CardLayout';
+import { IContractListItem } from '../common/interfaces/contract.interface';
+import i18n from '../common/config/i18n';
+import {
+ ArrowDownTrayIcon,
+ CheckIcon,
+ EyeIcon,
+ PlusIcon,
+ TrashIcon,
+ XMarkIcon,
+} from '@heroicons/react/24/outline';
+import { SortOrder, TableColumn } from 'react-data-table-component';
+import {
+ useApproveContractMutation,
+ useContractsQuery,
+ useDeleteContractMutation,
+ useRejectContractMutation,
+} from '../services/contracts/contracts.service';
+import { OrderDirection } from '../common/enums/order-direction.enum';
+import { ContractsTableBasicProps } from '../containers/query/ContractsTableWithQueryParams';
+import Popover from './Popover';
+import { useErrorToast, useSuccessToast } from '../hooks/useToast';
+import { InternalErrors } from '../common/errors/internal-errors.class';
+import Button from './Button';
+import {
+ ContractStatusMarkerColorMapper,
+ downloadExcel,
+ downloadFile,
+ formatDate,
+} from '../common/utils/utils';
+import LinkCell from './LinkCell';
+import CellLayout from '../layouts/CellLayout';
+import StatusWithMarker from './StatusWithMarker';
+import DataTableFilters from './DataTableFilters';
+import VolunteerSelect from '../containers/VolunteerSelect';
+import FormDatePicker from './FormDatePicker';
+import { ListItem } from '../common/interfaces/list-item.interface';
+import Select, { SelectItem } from './Select';
+import { ContractStatus } from '../common/enums/contract-status.enum';
+import { useNavigate } from 'react-router-dom';
+import ContractSidePanel from './ContractSidePanel';
+import ConfirmationModal from './ConfirmationModal';
+import RejectTextareaModal from './RejectTextareaModal';
+import { VolunteerTabsOptions } from '../pages/Volunteer';
+import { useTranslation } from 'react-i18next';
+import { getContractsForDownload } from '../services/contracts/contracts.api';
+import UploadFileModal from './UploadFileModal';
+
+const StatusOptions: SelectItem[] = [
+ {
+ key: ContractStatus.ACTIVE,
+ value: `${i18n.t(`documents:contract.status.${ContractStatus.ACTIVE}`)}`,
+ },
+ {
+ key: ContractStatus.CLOSED,
+ value: `${i18n.t(`documents:contract.status.${ContractStatus.CLOSED}`)}`,
+ },
+ {
+ key: ContractStatus.NOT_STARTED,
+ value: `${i18n.t(`documents:contract.status.${ContractStatus.NOT_STARTED}`)}`,
+ },
+ {
+ key: ContractStatus.REJECTED,
+ value: `${i18n.t(`documents:contract.status.${ContractStatus.REJECTED}`)}`,
+ },
+ {
+ key: ContractStatus.PENDING_ADMIN,
+ value: `${i18n.t(`documents:contract.status.${ContractStatus.PENDING_ADMIN}`)}`,
+ },
+ {
+ key: ContractStatus.PENDING_VOLUNTEER,
+ value: `${i18n.t(`documents:contract.status.${ContractStatus.PENDING_VOLUNTEER}`)}`,
+ },
+];
+
+const ContractsTableHeader = [
+ {
+ id: 'contractNumber',
+ name: i18n.t('documents:contracts.headers.contract_number'),
+ sortable: true,
+ selector: (row: IContractListItem) => row.contractNumber,
+ },
+ {
+ id: 'volunteer',
+ name: i18n.t('documents:contracts.headers.volunteer'),
+ grow: 2,
+ sortable: true,
+ cell: (row: IContractListItem) => (
+ {row.volunteer.name}
+ ),
+ },
+ {
+ id: 'startDate',
+ name: i18n.t('documents:contracts.headers.start_date'),
+ sortable: true,
+ selector: (row: IContractListItem) => formatDate(row.startDate),
+ },
+ {
+ id: 'endDate',
+ name: i18n.t('documents:contracts.headers.end_date'),
+ sortable: true,
+ selector: (row: IContractListItem) => formatDate(row.endDate),
+ },
+ {
+ id: 'status',
+ name: i18n.t('documents:contracts.headers.status'),
+ minWidth: '11rem',
+ sortable: true,
+ cell: (row: IContractListItem) => (
+
+
+ {i18n.t(`documents:contract.status.${row.status}`)}
+
+
+ ),
+ },
+];
+
+const VolunteerContractsTableHeader = [
+ {
+ id: 'contractNumber',
+ name: i18n.t('documents:contracts.headers.contract_number'),
+ sortable: true,
+ grow: 3,
+ selector: (row: IContractListItem) => row.contractNumber,
+ },
+ {
+ id: 'status',
+ name: i18n.t('documents:contracts.headers.status'),
+ minWidth: '11rem',
+ sortable: true,
+ cell: (row: IContractListItem) => (
+
+
+ {i18n.t(`documents:contract.status.${row.status}`)}
+
+
+ ),
+ },
+ {
+ id: 'startDate',
+ name: i18n.t('documents:contracts.headers.start_date'),
+ sortable: true,
+ selector: (row: IContractListItem) => formatDate(row.startDate),
+ },
+ {
+ id: 'endDate',
+ name: i18n.t('documents:contracts.headers.end_date'),
+ sortable: true,
+ selector: (row: IContractListItem) => formatDate(row.endDate),
+ },
+];
+
+interface ContractsTableProps extends ContractsTableBasicProps {
+ volunteerName?: string;
+ volunteerId?: string;
+}
+
+const ContractsTable = ({ query, setQuery, volunteerName, volunteerId }: ContractsTableProps) => {
+ // selected contract id
+ const [selectedContract, setSelectedContract] = useState();
+ // side panel state
+ const [isViewContractSidePanelOpen, setIsViewContractSidePanelOpen] = useState(false);
+ // confirmation modals
+ const [showRejectContract, setShowRejectContract] = useState(null);
+ const [showDeleteContract, setShowDeleteContract] = useState(null);
+ const [showApproveContract, setShowApproveContract] = useState(null);
+ // translation
+ const { t } = useTranslation('documents');
+ // navigation
+ const navigate = useNavigate();
+
+ //Actions
+ const { mutateAsync: deleteContract, isLoading: isDeletingContract } =
+ useDeleteContractMutation();
+
+ const { mutateAsync: approveContract, isLoading: isApprovingContract } =
+ useApproveContractMutation();
+
+ const { mutateAsync: rejectContract, isLoading: isRejectingContract } =
+ useRejectContractMutation();
+
+ const {
+ data: contracts,
+ isLoading: isContractsLoading,
+ error,
+ refetch,
+ } = useContractsQuery({
+ limit: query?.limit as number,
+ page: query?.page as number,
+ orderBy: query?.orderBy as string,
+ orderDirection: query?.orderDirection as OrderDirection,
+ search: query?.search,
+ volunteerName: query?.volunteer,
+ startDate: query?.startDate,
+ endDate: query?.endDate,
+ status: query?.status as ContractStatus,
+ volunteerId,
+ });
+
+ // query error handling
+ useEffect(() => {
+ if (error)
+ useErrorToast(InternalErrors.CONTRACT_ERRORS.getError(error.response?.data.code_error));
+ }, [error]);
+
+ const onView = (row: IContractListItem) => {
+ setSelectedContract(row.id);
+ setIsViewContractSidePanelOpen(true);
+ };
+
+ const onExport = async () => {
+ const { data } = await getContractsForDownload({
+ orderBy: query?.orderBy as string,
+ orderDirection: query?.orderDirection as OrderDirection,
+ search: query?.search,
+ volunteerName: query?.volunteer,
+ startDate: query?.startDate,
+ endDate: query?.endDate,
+ status: query?.status as ContractStatus,
+ volunteerId,
+ });
+
+ downloadExcel(data as BlobPart, t('contracts.download'));
+ };
+
+ const onRejectContract = (row: IContractListItem) => {
+ setShowRejectContract(row);
+ };
+
+ const onRemove = (row: IContractListItem) => {
+ setShowDeleteContract(row);
+ };
+
+ const buildContractActionColumn = (): TableColumn => {
+ const contractsMenuItems = [
+ {
+ label: t('events:popover.view'),
+ icon: ,
+ onClick: onView,
+ },
+ {
+ label: t('general:download', { item: i18n.t('general:contract').toLowerCase() }),
+ icon: ,
+ onClick: onDownloadContract,
+ },
+ ];
+
+ const contractsValidateOngMenuItems = [
+ ...contractsMenuItems,
+ {
+ label: t('popover.confirm'),
+ icon: ,
+ onClick: setShowApproveContract,
+ },
+ {
+ label: t('popover.reject'),
+ icon: ,
+ onClick: onRejectContract,
+ },
+ {
+ label: t('popover.remove'),
+ icon: ,
+ alert: true,
+ onClick: onRemove,
+ },
+ ];
+
+ const contractsValidateVolunteerMenuItems = [
+ ...contractsMenuItems,
+ {
+ label: t('popover.remove'),
+ icon: ,
+ alert: true,
+ onClick: onRemove,
+ },
+ ];
+
+ const contractsRejectedMenuItems = [
+ {
+ label: t('events:popover.view'),
+ icon: ,
+ onClick: onView,
+ },
+ {
+ label: t('popover.remove_from_list'),
+ icon: ,
+ alert: true,
+ onClick: onRemove,
+ },
+ ];
+
+ const mapContractStatusToPopoverItems = (status: ContractStatus) => {
+ switch (status) {
+ case ContractStatus.ACTIVE:
+ case ContractStatus.CLOSED:
+ case ContractStatus.NOT_STARTED:
+ return contractsMenuItems;
+ case ContractStatus.PENDING_ADMIN:
+ return contractsValidateOngMenuItems;
+ case ContractStatus.PENDING_VOLUNTEER:
+ return contractsValidateVolunteerMenuItems;
+ case ContractStatus.REJECTED:
+ return contractsRejectedMenuItems;
+ default:
+ return [];
+ }
+ };
+
+ return {
+ name: '',
+ cell: (row: IContractListItem) => (
+ row={row} items={mapContractStatusToPopoverItems(row.status)} />
+ ),
+ width: '50px',
+ allowOverflow: true,
+ };
+ };
+
+ const buildContractTableHeader = (): TableColumn[] => {
+ return volunteerName ? VolunteerContractsTableHeader : ContractsTableHeader;
+ };
+
+ // pagination
+ const onRowsPerPageChange = (rows: number) => {
+ setQuery({
+ limit: rows,
+ page: 1,
+ });
+ };
+
+ const onChangePage = (newPage: number) => {
+ setQuery({
+ page: newPage,
+ });
+ };
+
+ const onSort = (column: TableColumn, direction: SortOrder) => {
+ setQuery({
+ orderBy: column.id as string,
+ orderDirection:
+ direction.toLocaleUpperCase() === OrderDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC,
+ });
+ };
+
+ const onDownloadContract = (row: IContractListItem) => {
+ downloadFile(row.uri, row.fileName);
+ };
+
+ const onAddContract = () => {
+ navigate(`/documents/templates/contracts/generate`);
+ };
+
+ const onStartDateChange = (startDate: Date | null) => {
+ setQuery({ startDate: startDate as Date });
+ };
+
+ const onEndDateChange = (endDate: Date | null) => {
+ setQuery({ endDate: endDate as Date });
+ };
+
+ const onVolunteerChange = (volunteer: ListItem) => {
+ setQuery({ volunteer: volunteer.label });
+ };
+
+ const onResetFilters = () => {
+ if (volunteerName) {
+ setQuery({ activeTab: VolunteerTabsOptions.DOCUMENTS }, 'push');
+ } else {
+ setQuery({}, 'push');
+ }
+ };
+
+ const onSearch = (search: string) => {
+ setQuery({
+ search,
+ });
+ };
+
+ const onStatusChange = (item: SelectItem | undefined) => {
+ setQuery({ status: item?.key });
+ };
+
+ const onCloseSidePanel = (shouldRefetch?: boolean) => {
+ setIsViewContractSidePanelOpen(false);
+ setSelectedContract(undefined);
+ if (shouldRefetch) refetch();
+ };
+
+ const confirmReject = (rejectMessage?: string) => {
+ if (showRejectContract)
+ rejectContract(
+ {
+ id: showRejectContract.id,
+ rejectMessage,
+ },
+ {
+ onSuccess: () => {
+ useSuccessToast(t('contract.submit.reject'));
+ refetch();
+ },
+ onError: (error) => {
+ useErrorToast(InternalErrors.CONTRACT_ERRORS.getError(error.response?.data.code_error));
+ },
+ onSettled: () => {
+ setShowRejectContract(null);
+ },
+ },
+ );
+ };
+
+ const confirmDelete = () => {
+ if (showDeleteContract) {
+ const contractId = showDeleteContract.id;
+ setShowDeleteContract(null);
+ deleteContract(contractId, {
+ onSuccess: () => {
+ useSuccessToast(t('contract.submit.delete'));
+ refetch();
+ },
+ onError: (error) => {
+ useErrorToast(InternalErrors.CONTRACT_ERRORS.getError(error.response?.data.code_error));
+ },
+ });
+ }
+ };
+
+ const onConfirmSign = (contract?: File) => {
+ if (!contract) return;
+
+ // store id and close modal
+ const contractId = showApproveContract?.id;
+ setShowApproveContract(null);
+
+ // approval process
+ approveContract(
+ {
+ id: contractId as string,
+ contract,
+ },
+ {
+ onSuccess: () => {
+ useSuccessToast(t('contract.submit.confirm'));
+ },
+ onError: (error) => {
+ useErrorToast(InternalErrors.CONTRACT_ERRORS.getError(error.response?.data.code_error));
+ },
+ },
+ );
+ };
+
+ return (
+ <>
+
+ {!volunteerName && (
+
+ )}
+
+
+ option.key === query.status)}
+ />
+
+
+
+ {t('contracts.statistics.total', { total: contracts?.meta.totalItems })}
+
+ }
+ onClick={onExport}
+ />
+ }
+ onClick={onAddContract}
+ />
+
+
+
+
+
+ {showRejectContract && (
+
+ )}
+ {showDeleteContract && (
+
+ )}
+ {showApproveContract && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ContractsTable;
diff --git a/frontend/src/components/NewTemplatesTable.tsx b/frontend/src/components/NewTemplatesTable.tsx
new file mode 100644
index 000000000..61aee2ffa
--- /dev/null
+++ b/frontend/src/components/NewTemplatesTable.tsx
@@ -0,0 +1,208 @@
+import React from 'react';
+import { IPaginationQueryParams } from '../common/constants/pagination';
+import { IHOCQueryProps } from '../common/interfaces/hoc-query-props.interface';
+import Card from '../layouts/CardLayout';
+import CardHeader from './CardHeader';
+import Button from './Button';
+import { useTranslation } from 'react-i18next';
+import { ArrowDownTrayIcon, EyeIcon, PlusIcon } from '@heroicons/react/24/outline';
+import { useNavigate } from 'react-router-dom';
+import CardBody from './CardBody';
+import DataTableComponent from './DataTableComponent';
+import { ContractTemplate } from '../pages/ContractTemplates';
+import { SortOrder, TableColumn } from 'react-data-table-component';
+import Popover from './Popover';
+import { OrderDirection } from '../common/enums/order-direction.enum';
+
+export interface TemplatesTableQueryProps extends IPaginationQueryParams {
+ name?: string;
+ uses?: number;
+ lastUseDate?: Date;
+ createdBy?: string;
+ createdAt?: Date;
+}
+
+export type TemplatesTableProps = IHOCQueryProps;
+
+const ContractTemplatesTableHeader = [
+ {
+ id: 'name',
+ name: 'Nume',
+ sortable: true,
+ grow: 4,
+ minWidth: '9rem',
+ selector: (row: ContractTemplate) => row.name,
+ },
+ {
+ id: 'uses',
+ name: 'Utilizări',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ // todo: get uses count
+ selector: () => 'TODO',
+ },
+ {
+ id: 'last_used',
+ name: 'Ultima utilizare',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ // todo: get last usage count
+ selector: () => 'TODO',
+ },
+ {
+ id: 'created_by',
+ name: 'Creat de',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ selector: (row: ContractTemplate) => row.createdByAdmin.name,
+ },
+ {
+ id: 'created_at',
+ name: 'Data creării',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ // todo: get created_at date
+ selector: () => 'TODO',
+ },
+];
+
+const NewTemplatesTable = ({ query, setQuery }: TemplatesTableProps) => {
+ const { t } = useTranslation('volunteering_contracts');
+ const navigate = useNavigate();
+
+ // todo: remove
+ console.log(query);
+
+ const templates: ContractTemplate[] = [
+ {
+ id: '9a827436-e0b8-4763-8b04-ccff3f9b2757',
+ name: 'Test Template Lucia',
+ organizationData: {
+ CUI: '1278133',
+ officialName: 'Tenebru Diamonds Industry',
+ registeredOffice: 'Strada Cazarmii NR. 3392',
+ legalRepresentativeName: 'John Dave',
+ legalRepresentativeRole: 'Admin',
+ },
+ documentTerms: 'Contract terms
',
+ createdByAdmin: {
+ id: '8f2a561d-982f-465f-8dfb-bb2e16c39be6',
+ name: 'Galdo Gerald',
+ },
+ },
+ {
+ id: 'b3c45678-d9e0-4f12-a3b4-56c7d8e9f012',
+ name: 'Standard Contract Template',
+ organizationData: {
+ CUI: '9876543',
+ officialName: 'Global Solutions Inc.',
+ registeredOffice: 'Main Street 123',
+ legalRepresentativeName: 'Jane Smith',
+ legalRepresentativeRole: 'CEO',
+ },
+ documentTerms: 'Standard contract terms and conditions
',
+ createdByAdmin: {
+ id: '1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p',
+ name: 'Admin User',
+ },
+ },
+ {
+ id: 'c7d89012-e3f4-5g6h-i7j8-9k0l1m2n3o4p',
+ name: 'Volunteer Agreement Template',
+ organizationData: {
+ CUI: '5432109',
+ officialName: 'Community Helpers Association',
+ registeredOffice: 'Volunteer Avenue 456',
+ legalRepresentativeName: 'Mark Johnson',
+ legalRepresentativeRole: 'Director',
+ },
+ documentTerms: 'Volunteer agreement terms
',
+ createdByAdmin: {
+ id: 'q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6',
+ name: 'Sarah Admin',
+ },
+ },
+ ];
+
+ const buildActiveVolunteersActionColumn = (): TableColumn => {
+ const contractTemplateMenuItems = [
+ {
+ label: 'Vizualizează',
+ icon: ,
+ onClick: onView,
+ },
+ ];
+
+ return {
+ name: '',
+ cell: (row: ContractTemplate) => (
+ row={row} items={contractTemplateMenuItems} />
+ ),
+ width: '50px',
+ allowOverflow: true,
+ };
+ };
+
+ const onView = (row: ContractTemplate) => {
+ navigate(`/documents/templates/${row.id}`);
+ };
+
+ const onSort = (column: TableColumn, direction: SortOrder) => {
+ setQuery({
+ orderBy: column.id as string,
+ orderDirection:
+ direction.toLocaleUpperCase() === OrderDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC,
+ });
+ };
+
+ const onChangePage = (newPage: number) => {
+ setQuery({
+ page: newPage,
+ });
+ };
+
+ return (
+
+
+
+
{t('table_header.title')}
+ }
+ />
+ }
+ onClick={() => navigate('/documents/templates/create')}
+ />
+
+
+
+
+
+
+ );
+};
+
+export default NewTemplatesTable;
diff --git a/frontend/src/components/RichText/RichTextEditor.tsx b/frontend/src/components/RichText/RichTextEditor.tsx
index 4aeac4384..3d2e38a71 100644
--- a/frontend/src/components/RichText/RichTextEditor.tsx
+++ b/frontend/src/components/RichText/RichTextEditor.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
@@ -6,22 +7,28 @@ import 'react-quill/dist/quill.snow.css';
const RichTextEditor = ({
value,
onChange,
+ error,
+ className,
}: {
onChange: (value: string) => void;
value: string;
+ error?: FieldError | Merge | undefined;
+ className?: string;
}) => {
const { t } = useTranslation('doc_templates');
+ console.log('error', error);
return (
{t('text_editor.title')}
+ {error &&
{error.message?.toString()}
}
);
diff --git a/frontend/src/components/StatisticsCard.tsx b/frontend/src/components/StatisticsCard.tsx
index 52a68b011..a36b58f39 100644
--- a/frontend/src/components/StatisticsCard.tsx
+++ b/frontend/src/components/StatisticsCard.tsx
@@ -8,11 +8,12 @@ interface StatisticsCardProps {
action?: { label: string; onClick: () => void };
icon?: ReactNode;
info?: ReactNode;
+ className?: string;
}
-const StatisticsCard = ({ label, value, action, icon, info }: StatisticsCardProps) => {
+const StatisticsCard = ({ label, value, action, icon, info, className }: StatisticsCardProps) => {
return (
-
+
{icon}
diff --git a/frontend/src/components/contracts/ContractTemplatePreview.tsx b/frontend/src/components/contracts/ContractTemplatePreview.tsx
index 36b5366ee..e3b5c5c6e 100644
--- a/frontend/src/components/contracts/ContractTemplatePreview.tsx
+++ b/frontend/src/components/contracts/ContractTemplatePreview.tsx
@@ -6,8 +6,19 @@ import { Signatures } from '../Signatures';
import { useOrganizationQuery } from '../../services/organization/organization.service';
import LoadingContent from '../LoadingContent';
import { OrganizationDataError } from '../OrganizationDataError';
+import { Control, FieldErrors, FieldValues, UseFormGetValues, UseFormReset } from 'react-hook-form';
-export const ContractTemplatePreview = () => {
+export const ContractTemplatePreview = ({
+ control,
+ reset,
+ getValues,
+ formErrors,
+}: {
+ control: Control;
+ reset: UseFormReset;
+ getValues: UseFormGetValues;
+ formErrors: FieldErrors;
+}) => {
const { t } = useTranslation('doc_templates');
const [infoParagraphHovered, setInfoParagraphHovered] = useState(false);
@@ -141,7 +152,12 @@ export const ContractTemplatePreview = () => {
/>
-
+
);
diff --git a/frontend/src/components/contracts/ContractTerms.tsx b/frontend/src/components/contracts/ContractTerms.tsx
index a2991f074..04f757aef 100644
--- a/frontend/src/components/contracts/ContractTerms.tsx
+++ b/frontend/src/components/contracts/ContractTerms.tsx
@@ -1,22 +1,39 @@
import React, { useState } from 'react';
-import { Controller, FieldValues, SubmitHandler, useForm } from 'react-hook-form';
+import {
+ Control,
+ Controller,
+ FieldErrors,
+ FieldValues,
+ UseFormGetValues,
+ UseFormReset,
+} 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');
+export const ContractTerms = ({
+ control,
+ reset,
+ getValues,
+ formErrors,
+}: {
+ control: Control
;
+ reset: UseFormReset;
+ getValues: UseFormGetValues;
+ formErrors: FieldErrors;
+}) => {
+ const { t } = useTranslation(['doc_templates', 'general']);
const [editingText, setEditingText] = useState(false);
// todo: initial value for contractTerms taken from the template
const [contractTerms, setContractTerms] = useState('');
- const onSubmit: SubmitHandler = ({ contractTerms }) => {
+ const onSave = () => {
+ const newContractTerms = getValues('contractTerms');
// save the new value
- setContractTerms(contractTerms);
+ setContractTerms(newContractTerms);
// close text editor
setEditingText(false);
};
@@ -28,15 +45,16 @@ export const ContractTerms = () => {
void };
}) => {
- return ;
+ return (
+
+ );
}}
/>
@@ -53,7 +71,7 @@ export const ContractTerms = () => {
@@ -61,8 +79,9 @@ export const ContractTerms = () => {
}
if (!contractTerms) {
- return ;
- }
- // normal text
+ return (
+
+ );
+ } // normal text
return ;
};
diff --git a/frontend/src/components/contracts/ContractTermsEmptyState.tsx b/frontend/src/components/contracts/ContractTermsEmptyState.tsx
index f27003dd6..0aed4e3d1 100644
--- a/frontend/src/components/contracts/ContractTermsEmptyState.tsx
+++ b/frontend/src/components/contracts/ContractTermsEmptyState.tsx
@@ -2,17 +2,23 @@ import React from 'react';
import Button from '../Button';
import { PencilIcon } from '@heroicons/react/24/solid';
import { useTranslation } from 'react-i18next';
+import { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form';
export const ContractTermsEmptyState = ({
setEditingText,
+ error,
}: {
setEditingText: React.Dispatch>;
+ error: FieldError | Merge | undefined;
}) => {
const { t } = useTranslation('doc_templates');
return (
-
+
{t('contract_terms.title')}
+ {error &&
{error.message?.toString()}
}
{t('contract_terms.describe')}
diff --git a/frontend/src/components/contracts/OrganizationDetails.tsx b/frontend/src/components/contracts/OrganizationDetails.tsx
index 31565278f..5027603b4 100644
--- a/frontend/src/components/contracts/OrganizationDetails.tsx
+++ b/frontend/src/components/contracts/OrganizationDetails.tsx
@@ -2,14 +2,13 @@ import React from 'react';
import FormInput from '../FormInput';
import { useTranslation } from 'react-i18next';
import { OrganizationDataForm } from './OrganizationDataForm';
-import { Controller, useForm } from 'react-hook-form';
+import { Control, Controller, FieldValues } 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();
+export const OrganizationDetails = ({ control }: { control: Control
}) => {
+ const { t } = useTranslation(['doc_templates', 'general']);
const queryClient = useQueryClient();
@@ -25,9 +24,20 @@ export const OrganizationDetails = () => {
name="templateName"
control={control}
rules={{ required: true }}
- render={({ field: { value, onChange } }) => (
-
- )}
+ render={({ field: { value, onChange }, formState: { errors } }) => {
+ return (
+
+ );
+ }}
/>
{t('organization.data')}
diff --git a/frontend/src/containers/query/NewContractsTableWithQueryParams.tsx b/frontend/src/containers/query/NewContractsTableWithQueryParams.tsx
new file mode 100644
index 000000000..306ada438
--- /dev/null
+++ b/frontend/src/containers/query/NewContractsTableWithQueryParams.tsx
@@ -0,0 +1,56 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import React from 'react';
+import { DateParam, QueryParams, StringParam } from 'use-query-params';
+import { IHOCQueryProps } from '../../common/interfaces/hoc-query-props.interface';
+import {
+ IPaginationQueryParams,
+ getPaginationQueryParams,
+} from '../../common/constants/pagination';
+import { ContractStatus } from '../../common/enums/contract-status.enum';
+import { VolunteerTabsOptions } from '../../pages/Volunteer';
+import NewContractsTable from '../../components/NewContractsTable';
+
+export interface ContractsTableQueryProps extends IPaginationQueryParams {
+ volunteer?: string;
+ search?: string;
+ startDate?: Date;
+ endDate?: Date;
+ status?: ContractStatus;
+ activeTab?: VolunteerTabsOptions;
+}
+
+export type ContractsTableBasicProps = IHOCQueryProps;
+
+// set page default params
+const DEFAULT_QUERY_PARAMS = getPaginationQueryParams();
+
+const ContractsTableWithQueryParams = ({
+ volunteerName,
+ volunteerId,
+}: {
+ volunteerName?: string;
+ volunteerId?: string;
+}) => {
+ const queryConfig = {
+ ...DEFAULT_QUERY_PARAMS,
+ volunteer: StringParam,
+ search: StringParam,
+ startDate: DateParam,
+ endDate: DateParam,
+ status: StringParam,
+ activeTab: StringParam,
+ };
+
+ return (
+
+ {(props: any) => {
+ return (
+ // !THIS USES THE OLD COMPONENT, IT'S JUST A COPY OF THAT
+
+ );
+ }}
+
+ );
+};
+
+export default ContractsTableWithQueryParams;
diff --git a/frontend/src/containers/query/NewTemplatesTableWithQueryParams.tsx b/frontend/src/containers/query/NewTemplatesTableWithQueryParams.tsx
new file mode 100644
index 000000000..6fdd32be7
--- /dev/null
+++ b/frontend/src/containers/query/NewTemplatesTableWithQueryParams.tsx
@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import React from 'react';
+import { getPaginationQueryParams } from '../../common/constants/pagination';
+import { DateParam, NumberParam, QueryParams, StringParam } from 'use-query-params';
+import NewTemplatesTable from '../../components/NewTemplatesTable';
+
+const DEFAULT_QUERY_PARAMS = getPaginationQueryParams();
+
+export const TemplatesTableWithQueryParams = () => {
+ const queryConfig = {
+ ...DEFAULT_QUERY_PARAMS,
+ name: StringParam,
+ uses: NumberParam,
+ lastUseDate: DateParam,
+ createdBy: StringParam,
+ createdAt: DateParam,
+ };
+ return (
+
+ {(props: any) => {
+ return ;
+ }}
+
+ );
+};
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 09d7fea37..afa3d3fc5 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -334,6 +334,18 @@ li.recharts-legend-item {
z-index: 1000;
}
+.ql-container {
+ @apply border-0;
+}
+
+.ql-container.ql-snow {
+ @apply border-0;
+}
+
+.ql-toolbar.ql-snow {
+ @apply border-0 border-b-2 border-cool-gray-900;
+}
+
@layer utilities {
/* Hide scrollbar for Chrome, Safari, and Opera */
.no-scrollbar::-webkit-scrollbar {
diff --git a/frontend/src/pages/ContractTemplates.tsx b/frontend/src/pages/ContractTemplates.tsx
index 8ea1b71a8..3698c94e2 100644
--- a/frontend/src/pages/ContractTemplates.tsx
+++ b/frontend/src/pages/ContractTemplates.tsx
@@ -1,79 +1,127 @@
-import React from 'react';
+import React, { useState } from 'react';
+import { useContractTemplateQuery } from '../services/documents-templates/documents-templates.service';
import PageLayout from '../layouts/PageLayout';
import PageHeader from '../components/PageHeader';
-import { useNavigate } from 'react-router-dom';
-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 '../components/contracts/OrganizationDetails';
-import { ContractTemplatePreview } from '../components/contracts/ContractTemplatePreview';
import { useTranslation } from 'react-i18next';
+import Tabs from '../components/Tabs';
+import { ContractType } from '../common/enums/contract-type.enum';
+import { ContractsStatistics } from '../components/ContractsStatistics';
+import NewContractsTableWithQueryParams from '../containers/query/NewContractsTableWithQueryParams';
+import { TemplatesTableWithQueryParams } from '../containers/query/NewTemplatesTableWithQueryParams';
-export const ContractTemplates = () => {
- const navigate = useNavigate();
+interface OrganizationData {
+ CUI: string;
+ officialName: string;
+ registeredOffice: string;
+ legalRepresentativeName: string;
+ legalRepresentativeRole: string;
+}
- const { t } = useTranslation('doc_templates');
+interface CreatedByAdmin {
+ id: string;
+ name: string;
+}
- const navigateBack = () => {
- navigate('/documents/contracts', { replace: true });
- };
+export interface ContractTemplate {
+ id: string;
+ name: string;
+ organizationData: OrganizationData;
+ documentTerms: string;
+ createdByAdmin: CreatedByAdmin;
+}
- // TODO: links for
+const templates: ContractTemplate[] = [
+ {
+ id: '9a827436-e0b8-4763-8b04-ccff3f9b2757',
+ name: 'Test Template Lucia',
+ organizationData: {
+ CUI: '1278133',
+ officialName: 'Tenebru Diamonds Industry',
+ registeredOffice: 'Strada Cazarmii NR. 3392',
+ legalRepresentativeName: 'John Dave',
+ legalRepresentativeRole: 'Admin',
+ },
+ documentTerms: 'Contract terms
',
+ createdByAdmin: {
+ id: '8f2a561d-982f-465f-8dfb-bb2e16c39be6',
+ name: 'Galdo Gerald',
+ },
+ },
+ {
+ id: 'b3c45678-d9e0-4f12-a3b4-56c7d8e9f012',
+ name: 'Standard Contract Template',
+ organizationData: {
+ CUI: '9876543',
+ officialName: 'Global Solutions Inc.',
+ registeredOffice: 'Main Street 123',
+ legalRepresentativeName: 'Jane Smith',
+ legalRepresentativeRole: 'CEO',
+ },
+ documentTerms: 'Standard contract terms and conditions
',
+ createdByAdmin: {
+ id: '1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p',
+ name: 'Admin User',
+ },
+ },
+ {
+ id: 'c7d89012-e3f4-5g6h-i7j8-9k0l1m2n3o4p',
+ name: 'Volunteer Agreement Template',
+ organizationData: {
+ CUI: '5432109',
+ officialName: 'Community Helpers Association',
+ registeredOffice: 'Volunteer Avenue 456',
+ legalRepresentativeName: 'Mark Johnson',
+ legalRepresentativeRole: 'Director',
+ },
+ documentTerms: 'Volunteer agreement terms
',
+ createdByAdmin: {
+ id: 'q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6',
+ name: 'Sarah Admin',
+ },
+ },
+];
- return (
-
- {t('title')}
+export const ContractTemplates = () => {
+ const { t } = useTranslation('volunteering_contracts');
+ const { data: contractTemplate } = useContractTemplateQuery(templates[0].id);
+ console.log(contractTemplate);
+ // todo: probably modify this to use query params
+ const [activeTab, setActiveTab] = useState(ContractType.CONTRACT);
- {/* sub header text */}
-
+ // todo:
+ // get all contract templates
+ // loading state
+ // error state
- {/* TABLE */}
-
- {/* TABLE HEADER */}
-
-
-
{t('table_header.title')}
-
- }
- className="btn-outline-secondary text-cool-gray-600 "
- // TODO: descarca necompletat functionality
- onClick={() => {}}
- />
- {/* // TODO: save functionality */}
-
-
-
-
+ const onTabClick = (tab: ContractType) => {
+ setActiveTab(tab);
+ };
- {/* TABLE BODY */}
-
-
-
-
-
-
-
-
+ return (
+ <>
+
+ {t('title')}
+
+ tabs={[
+ { key: ContractType.CONTRACT, value: t('tabs.contracts') },
+ { key: ContractType.TEMPLATE, value: t('tabs.templates') },
+ ]}
+ onClick={onTabClick}
+ // defaultTab={DocumentsTabsOptions.find((tab) => tab.key === query?.contractType)}
+ >
+ {t('description')}
+
+ {activeTab === ContractType.TEMPLATE ? (
+
+ ) : (
+ <>
+
+ {/* //! THIS USES THE OLD COMPONENT, IT'S JUST A COPY OF THAT */}
+
+ >
+ )}
+
+
+ >
);
- {
- /* {isLoading && } */
- }
};
diff --git a/frontend/src/pages/CreateContractTemplate.tsx b/frontend/src/pages/CreateContractTemplate.tsx
new file mode 100644
index 000000000..a69484d29
--- /dev/null
+++ b/frontend/src/pages/CreateContractTemplate.tsx
@@ -0,0 +1,168 @@
+import React, { useMemo } from 'react';
+import PageLayout from '../layouts/PageLayout';
+import PageHeader from '../components/PageHeader';
+import { useNavigate } from 'react-router-dom';
+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 '../components/contracts/OrganizationDetails';
+import { ContractTemplatePreview } from '../components/contracts/ContractTemplatePreview';
+import { useTranslation } from 'react-i18next';
+import { FieldValues, useForm } from 'react-hook-form';
+import { useOrganizationQuery } from '../services/organization/organization.service';
+import { Tooltip } from 'react-tooltip';
+import {
+ IAddContractTemplatePayload,
+ useAddContractTemplateMutation,
+} from '../services/documents-templates/documents-templates.service';
+
+export const CreateContractTemplate = () => {
+ const navigate = useNavigate();
+
+ const { t } = useTranslation(['doc_templates', 'general']);
+ const {
+ control,
+ handleSubmit,
+ reset,
+ getValues,
+ setError,
+ formState: { errors },
+ } = useForm();
+ const { data: organization } = useOrganizationQuery();
+ const isOrganizationDataComplete = useMemo(
+ () =>
+ organization?.name &&
+ organization?.address &&
+ organization?.cui &&
+ organization?.legalReprezentativeFullName &&
+ organization?.legalReprezentativeRole,
+ [organization],
+ );
+ console.log('errors', errors);
+ const { mutate: addContractTemplate, isLoading: isLoadingAddContractTemplate } =
+ useAddContractTemplateMutation();
+
+ const onSubmit = ({ templateName, contractTerms }: FieldValues) => {
+ console.log('templateName', templateName);
+ console.log('contractTerms', contractTerms);
+ if (!contractTerms) {
+ return setError('contractTerms', {
+ type: 'required',
+ message: t('required', { ns: 'general' }),
+ });
+ }
+ const contractTemplateData = {
+ name: templateName,
+ organizationData: {
+ officialName: organization?.name,
+ registeredOffice: organization?.address,
+ CUI: organization?.cui,
+ legalRepresentativeName: organization?.legalReprezentativeFullName,
+ legalRepresentativeRole: organization?.legalReprezentativeRole,
+ },
+ documentTerms: contractTerms,
+ } as IAddContractTemplatePayload;
+ addContractTemplate(contractTemplateData, {
+ onSuccess: () => {
+ //todo
+ },
+ onError: () => {
+ //todo
+ },
+ });
+ };
+
+ const navigateBack = () => {
+ // todo: this should send us back to the templates tab
+ navigate('/documents/templates', { replace: true });
+ };
+
+ // TODO: links for
+
+ return (
+
+ {t('title')}
+
+ {/* sub header text */}
+
+
+
+
+
+
{t('table_header.title')}
+
+ }
+ className="btn-outline-secondary text-cool-gray-600 "
+ // TODO: descarca necompletat functionality
+ onClick={() => {}}
+ />
+ {/* // TODO: save functionality */}
+
+ {!isOrganizationDataComplete && (
+
+ )}
+
+
+
+
+ {/* TABLE BODY */}
+
+
+
+
+
+
+
+
+ );
+ {
+ /* {isLoading && } */
+ }
+};
diff --git a/frontend/src/pages/GenerateContract.tsx b/frontend/src/pages/GenerateContract.tsx
new file mode 100644
index 000000000..16ec54aa9
--- /dev/null
+++ b/frontend/src/pages/GenerateContract.tsx
@@ -0,0 +1,212 @@
+import React, { useMemo, useState } from 'react';
+import PageLayout from '../layouts/PageLayout';
+import PageHeader from '../components/PageHeader';
+import { useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { Stepper } from '../components/Stepper';
+import Button from '../components/Button';
+import CardBody from '../components/CardBody';
+import DataTableComponent from '../components/DataTableComponent';
+import { ContractTemplate } from './ContractTemplates';
+import Card from '../layouts/CardLayout';
+import CardHeader from '../components/CardHeader';
+
+const ContractTemplatesTableHeader = [
+ {
+ id: 'name',
+ name: 'Nume',
+ sortable: true,
+ grow: 4,
+ minWidth: '9rem',
+ selector: (row: ContractTemplate) => row.name,
+ },
+ {
+ id: 'uses',
+ name: 'Utilizări',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ // todo: get uses count
+ selector: () => 'TODO',
+ },
+ {
+ id: 'last_used',
+ name: 'Ultima utilizare',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ // todo: get last usage count
+ selector: () => 'TODO',
+ },
+ {
+ id: 'created_by',
+ name: 'Creat de',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ selector: (row: ContractTemplate) => row.createdByAdmin.name,
+ },
+ {
+ id: 'created_at',
+ name: 'Data creării',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ // todo: get created_at date
+ selector: () => 'TODO',
+ },
+];
+const templates: ContractTemplate[] = [
+ {
+ id: '9a827436-e0b8-4763-8b04-ccff3f9b2757',
+ name: 'Test Template Lucia',
+ organizationData: {
+ CUI: '1278133',
+ officialName: 'Tenebru Diamonds Industry',
+ registeredOffice: 'Strada Cazarmii NR. 3392',
+ legalRepresentativeName: 'John Dave',
+ legalRepresentativeRole: 'Admin',
+ },
+ documentTerms: 'Contract terms
',
+ createdByAdmin: {
+ id: '8f2a561d-982f-465f-8dfb-bb2e16c39be6',
+ name: 'Galdo Gerald',
+ },
+ },
+ {
+ id: 'b3c45678-d9e0-4f12-a3b4-56c7d8e9f012',
+ name: 'Standard Contract Template',
+ organizationData: {
+ CUI: '9876543',
+ officialName: 'Global Solutions Inc.',
+ registeredOffice: 'Main Street 123',
+ legalRepresentativeName: 'Jane Smith',
+ legalRepresentativeRole: 'CEO',
+ },
+ documentTerms: 'Standard contract terms and conditions
',
+ createdByAdmin: {
+ id: '1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p',
+ name: 'Admin User',
+ },
+ },
+ {
+ id: 'c7d89012-e3f4-5g6h-i7j8-9k0l1m2n3o4p',
+ name: 'Volunteer Agreement Template',
+ organizationData: {
+ CUI: '5432109',
+ officialName: 'Community Helpers Association',
+ registeredOffice: 'Volunteer Avenue 456',
+ legalRepresentativeName: 'Mark Johnson',
+ legalRepresentativeRole: 'Director',
+ },
+ documentTerms: 'Volunteer agreement terms
',
+ createdByAdmin: {
+ id: 'q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6',
+ name: 'Sarah Admin',
+ },
+ },
+];
+
+export const GenerateContract = () => {
+ const { t } = useTranslation(['volunteering_contracts', 'stepper']);
+ const navigate = useNavigate();
+
+ const steps = useMemo(
+ () => [
+ { id: '1', label: t('choose_template', { ns: 'stepper' }) },
+ { id: '2', label: t('choose_volunteers', { ns: 'stepper' }) },
+ { id: '3', label: t('fill', { ns: 'stepper' }) },
+ { id: '4', label: t('attachments', { ns: 'stepper' }) },
+ { id: '5', label: t('complete', { ns: 'stepper' }) },
+ ],
+ [],
+ );
+ 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);
+ }
+ };
+ const onSelectTemplate = (template: ContractTemplate) => {
+ console.log(template);
+ };
+
+ const renderStep = () => {
+ switch (currentStep) {
+ case 0:
+ return (
+
+
+ {t('templates')}
+
+
+
+
+
+ );
+ case 1:
+ return Volunteers
;
+ }
+ };
+
+ const navigateBack = () => {
+ navigate('/documents/templates', { replace: true });
+ };
+
+ return (
+
+ {t('generate.title')}
+
+ {/* template table */}
+ {renderStep()}
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/routes/Router.tsx b/frontend/src/routes/Router.tsx
index b9b70f561..ec7730378 100644
--- a/frontend/src/routes/Router.tsx
+++ b/frontend/src/routes/Router.tsx
@@ -35,8 +35,10 @@ import AddContractTemplate from '../pages/AddContractTemplate';
import EditContractTemplate from '../pages/EditContractTemplate';
import AddContract from '../containers/query/AddContractWithQueryParams';
import ActionsArchive from '../pages/ActionsArchive';
-import { ContractTemplates } from '../pages/ContractTemplates';
+import { CreateContractTemplate } from '../pages/CreateContractTemplate';
import { StepperExample } from '../pages/StepperExample';
+import { ContractTemplates } from '../pages/ContractTemplates';
+import { GenerateContract } from '../pages/GenerateContract';
const Router = () => {
return (
@@ -96,6 +98,11 @@ const Router = () => {
}>
} />
+ } />
+ }>
+ } />
+
+ {/* } /> */}
} />
diff --git a/frontend/src/services/documents-templates/documents-templates.api.ts b/frontend/src/services/documents-templates/documents-templates.api.ts
new file mode 100644
index 000000000..0e05ac6a7
--- /dev/null
+++ b/frontend/src/services/documents-templates/documents-templates.api.ts
@@ -0,0 +1,10 @@
+import { IAddContractTemplatePayload } from './documents-templates.service';
+import API from '../api';
+
+export const addContractTemplate = (data: IAddContractTemplatePayload) => {
+ return API.post('/documents/templates', data).then((res) => res.data);
+};
+
+export const getContractTemplate = (id: string) => {
+ return API.get(`/documents/templates/${id}`).then((res) => res.data);
+};
diff --git a/frontend/src/services/documents-templates/documents-templates.service.ts b/frontend/src/services/documents-templates/documents-templates.service.ts
new file mode 100644
index 000000000..9cee26a93
--- /dev/null
+++ b/frontend/src/services/documents-templates/documents-templates.service.ts
@@ -0,0 +1,38 @@
+import { useMutation, useQuery } from 'react-query';
+import { addContractTemplate, getContractTemplate } from './documents-templates.api';
+
+interface IOrganizationData {
+ officialName: string;
+ registeredOffice: string;
+ CUI: string;
+ legalRepresentativeName: string;
+ legalRepresentativeRole: string;
+}
+
+export interface IAddContractTemplatePayload {
+ name: string;
+ organizationData: IOrganizationData;
+ documentTerms: string;
+}
+
+export const useAddContractTemplateMutation = () => {
+ return useMutation((data: IAddContractTemplatePayload) => addContractTemplate(data), {
+ //? do we add a more specific error type like in the contracts?
+ onError: (error) => {
+ console.log('⭕️ ERROR IN ADD CONTRACT MUTATION ⭕️', error);
+ return Promise.resolve(error);
+ },
+ });
+};
+
+export const useContractTemplateQuery = (id: string) => {
+ return useQuery({
+ queryKey: ['contractTemplate', id],
+ queryFn: () => getContractTemplate(id),
+ enabled: !!id,
+ onError: (error) => {
+ console.log('⭕️ ERROR IN GET CONTRACT TEMPLATE QUERY ⭕️', error);
+ return Promise.resolve(error);
+ },
+ });
+};
From 7bbbf77574b3107080d1d9d7c1688f6cf52c54b6 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Mon, 9 Sep 2024 13:51:56 +0300
Subject: [PATCH 16/55] feat: [Contracts] Contracts, Templates and Signatures
DB, Models, Repositories, Usecases, partial API
---
backend/src/api/api.module.ts | 2 +
.../documents/document-contract.controller.ts | 60 ++++++
.../documents/document-template.controller.ts | 4 +-
.../dto/create-document-contract.dto.ts | 28 +++
.../dto/get-many-document-contracts.dto.ts | 21 ++
...ument-contract-list-view-item.presenter.ts | 89 +++++++++
...878984294-DDLDocumentContractsSignature.ts | 135 +++++++++++++
.../src/modules/documents/documents.module.ts | 15 ++
.../document-contract-list-view.entity.ts | 54 +++++
.../entities/document-contract.entity.ts | 161 +++++++++++++++
.../entities/document-signature.entity.ts | 25 +++
.../entities/document-template.entity.ts | 16 +-
.../documents/enums/contract-status.enum.ts | 12 ++
.../document-contract-list-view.model.ts | 33 +++
.../models/document-contract.model.ts | 141 +++++++++++++
.../models/document-signature.model.ts | 21 ++
.../models/document-template.model.ts | 4 +-
.../document-contract-list-view.repository.ts | 86 ++++++++
.../document-contract.repository.ts | 72 +++++++
.../document-signature.repository.ts | 32 +++
.../services/document-contract.facade.ts | 51 +++++
.../create-document-contract.usecase.ts | 188 ++++++++++++++++++
.../create-document-template.usecase.ts | 0
.../get-many-document-contracts.usecase.ts | 23 +++
.../get-one-document-template.usecase.ts | 9 +-
backend/src/usecases/use-case.module.ts | 16 +-
26 files changed, 1283 insertions(+), 15 deletions(-)
create mode 100644 backend/src/api/documents/document-contract.controller.ts
create mode 100644 backend/src/api/documents/dto/create-document-contract.dto.ts
create mode 100644 backend/src/api/documents/dto/get-many-document-contracts.dto.ts
create mode 100644 backend/src/api/documents/presenters/document-contract-list-view-item.presenter.ts
create mode 100644 backend/src/migrations/1725878984294-DDLDocumentContractsSignature.ts
create mode 100644 backend/src/modules/documents/entities/document-contract-list-view.entity.ts
create mode 100644 backend/src/modules/documents/entities/document-contract.entity.ts
create mode 100644 backend/src/modules/documents/entities/document-signature.entity.ts
create mode 100644 backend/src/modules/documents/models/document-contract-list-view.model.ts
create mode 100644 backend/src/modules/documents/models/document-contract.model.ts
create mode 100644 backend/src/modules/documents/models/document-signature.model.ts
create mode 100644 backend/src/modules/documents/repositories/document-contract-list-view.repository.ts
create mode 100644 backend/src/modules/documents/repositories/document-contract.repository.ts
create mode 100644 backend/src/modules/documents/repositories/document-signature.repository.ts
create mode 100644 backend/src/modules/documents/services/document-contract.facade.ts
create mode 100644 backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts
rename backend/src/usecases/documents/{ => new_contracts}/create-document-template.usecase.ts (100%)
create mode 100644 backend/src/usecases/documents/new_contracts/get-many-document-contracts.usecase.ts
rename backend/src/usecases/documents/{ => new_contracts}/get-one-document-template.usecase.ts (79%)
diff --git a/backend/src/api/api.module.ts b/backend/src/api/api.module.ts
index f7f8eb073..26ed48ce6 100644
--- a/backend/src/api/api.module.ts
+++ b/backend/src/api/api.module.ts
@@ -32,6 +32,7 @@ import { MobileAnouncementsController } from './_mobile/anouncements/anouncement
import { MobileSettingsController } from './_mobile/settings/settings-controller';
import { MobileNewsController } from './_mobile/news/news.controller';
import { DocumentTemplateController } from './documents/document-template.controller';
+import { DocumentContractController } from './documents/document-contract.controller';
@Module({
imports: [UseCaseModule],
@@ -54,6 +55,7 @@ import { DocumentTemplateController } from './documents/document-template.contro
TemplateController,
ContractController,
DocumentTemplateController,
+ DocumentContractController,
// Mobile
MobileRegularUserController,
MobileAccessRequestController,
diff --git a/backend/src/api/documents/document-contract.controller.ts b/backend/src/api/documents/document-contract.controller.ts
new file mode 100644
index 000000000..612d96df2
--- /dev/null
+++ b/backend/src/api/documents/document-contract.controller.ts
@@ -0,0 +1,60 @@
+import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
+import { CreateDocumentContractUsecase } from 'src/usecases/documents/new_contracts/create-document-contract.usecase';
+import { CreateDocumentContractDto } from './dto/create-document-contract.dto';
+import { IAdminUserModel } from 'src/modules/user/models/admin-user.model';
+import { ExtractUser } from 'src/common/decorators/extract-user.decorator';
+import { ApiBearerAuth } from '@nestjs/swagger';
+import { WebJwtAuthGuard } from 'src/modules/auth/guards/jwt-web.guard';
+import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
+import { GetManyDocumentContractsUsecase } from 'src/usecases/documents/new_contracts/get-many-document-contracts.usecase';
+import { DocumentContractListViewItemPresenter } from './presenters/document-contract-list-view-item.presenter';
+import { PaginatedPresenter } from 'src/infrastructure/presenters/generic-paginated.presenter';
+import { GetManyDocumentContractsDto } from './dto/get-many-document-contracts.dto';
+
+@ApiBearerAuth()
+@UseGuards(WebJwtAuthGuard)
+@Controller('documents/contracts')
+export class DocumentContractController {
+ constructor(
+ private readonly createDocumentContractUsecase: CreateDocumentContractUsecase,
+ private readonly getManyDocumentContractsUsecase: GetManyDocumentContractsUsecase,
+ ) {}
+
+ @Post()
+ async createDocumentContract(
+ @Body() createDocumentContractDto: CreateDocumentContractDto,
+ @ExtractUser() { organizationId, id: adminId }: IAdminUserModel,
+ ): Promise {
+ const documentContract = await this.createDocumentContractUsecase.execute({
+ ...createDocumentContractDto,
+ organizationId,
+ createdByAdminId: adminId,
+ });
+
+ return documentContract;
+ }
+
+ @Get('')
+ async getDocumentContracts(
+ @Query() filters: GetManyDocumentContractsDto,
+ @ExtractUser() { organizationId }: IAdminUserModel,
+ ): Promise> {
+ const contracts = await this.getManyDocumentContractsUsecase.execute({
+ ...filters,
+ organizationId,
+ });
+
+ return new PaginatedPresenter({
+ ...contracts,
+ items: contracts.items.map(
+ (contract) => new DocumentContractListViewItemPresenter(contract),
+ ),
+ });
+ }
+
+ /* TODO: GET /documents/contracts/check?year={year}&documentNumber={documentNumber}
+ CHECK IF A CONTRACT ALREADY EXISTS FOR THE GIVEN YEAR AND DOCUMENT NUMBER IN THE SAME ORGANIZATION
+ RETURN TRUE IF IT EXISTS, FALSE OTHERWISE
+ USED TO PREVENT DUPLICATE DOCUMENT NUMBERS IN THE SAME YEAR
+ */
+}
diff --git a/backend/src/api/documents/document-template.controller.ts b/backend/src/api/documents/document-template.controller.ts
index da5de3563..e28aa12d2 100644
--- a/backend/src/api/documents/document-template.controller.ts
+++ b/backend/src/api/documents/document-template.controller.ts
@@ -5,9 +5,9 @@ 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';
+import { CreateDocumentTemplateUsecase } from 'src/usecases/documents/new_contracts/create-document-template.usecase';
+import { GetOneDocumentTemplateUseCase } from 'src/usecases/documents/new_contracts/get-one-document-template.usecase';
@ApiBearerAuth()
@UseGuards(WebJwtAuthGuard)
diff --git a/backend/src/api/documents/dto/create-document-contract.dto.ts b/backend/src/api/documents/dto/create-document-contract.dto.ts
new file mode 100644
index 000000000..d28549228
--- /dev/null
+++ b/backend/src/api/documents/dto/create-document-contract.dto.ts
@@ -0,0 +1,28 @@
+import { IsDate, IsEnum, IsString, MaxLength } from 'class-validator';
+import { DocumentContractStatus } from 'src/modules/documents/enums/contract-status.enum';
+
+export class CreateDocumentContractDto {
+ // TODO: validate dates
+
+ @IsEnum(DocumentContractStatus)
+ status: DocumentContractStatus;
+
+ @IsString()
+ @MaxLength(9)
+ documentNumber: string;
+
+ @IsDate()
+ documentDate: Date;
+
+ @IsDate()
+ documentStartDate: Date;
+
+ @IsDate()
+ documentEndDate: Date;
+
+ @IsString()
+ volunteerId: string;
+
+ @IsString()
+ documentTemplateId: string;
+}
diff --git a/backend/src/api/documents/dto/get-many-document-contracts.dto.ts b/backend/src/api/documents/dto/get-many-document-contracts.dto.ts
new file mode 100644
index 000000000..d03b5a126
--- /dev/null
+++ b/backend/src/api/documents/dto/get-many-document-contracts.dto.ts
@@ -0,0 +1,21 @@
+import { IsDate, IsEnum, IsOptional, IsString } from 'class-validator';
+import { BasePaginationFilterDto } from 'src/infrastructure/base/base-pagination-filter.dto';
+import { DocumentContractStatus } from 'src/modules/documents/enums/contract-status.enum';
+
+export class GetManyDocumentContractsDto extends BasePaginationFilterDto {
+ @IsOptional()
+ @IsString()
+ volunteerId?: string;
+
+ @IsOptional()
+ @IsEnum(DocumentContractStatus)
+ status?: DocumentContractStatus;
+
+ @IsDate()
+ @IsOptional()
+ startDate?: Date;
+
+ @IsDate()
+ @IsOptional()
+ endDate?: Date;
+}
diff --git a/backend/src/api/documents/presenters/document-contract-list-view-item.presenter.ts b/backend/src/api/documents/presenters/document-contract-list-view-item.presenter.ts
new file mode 100644
index 000000000..5c79d3270
--- /dev/null
+++ b/backend/src/api/documents/presenters/document-contract-list-view-item.presenter.ts
@@ -0,0 +1,89 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { Expose } from 'class-transformer';
+import { DocumentContractStatus } from 'src/modules/documents/enums/contract-status.enum';
+import { IDocumentContractListViewModel } from 'src/modules/documents/models/document-contract-list-view.model';
+
+export class DocumentContractListViewItemPresenter {
+ constructor(item: IDocumentContractListViewModel) {
+ this.documentId = item.documentId;
+ this.documentNumber = item.documentNumber;
+ this.documentStartDate = item.documentStartDate;
+ this.documentEndDate = item.documentEndDate;
+ this.documentFilePath = item.documentFilePath;
+ this.status = item.status;
+ this.volunteerId = item.volunteerId;
+ this.volunteerName = item.volunteerName;
+ this.organizationId = item.organizationId;
+ this.organizationName = item.organizationName;
+ }
+
+ @Expose()
+ @ApiProperty({
+ description: 'The uuid of the template',
+ example: '525dcdf9-4117-443e-a0c3-bf652cdc5c1b',
+ })
+ documentId: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The document number',
+ example: '123456',
+ })
+ documentNumber: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The document start date',
+ example: '2021-01-01',
+ })
+ documentStartDate: Date;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The document end date',
+ example: '2021-01-01',
+ })
+ documentEndDate: Date;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The document file path',
+ example: 'https://example.com/document.pdf',
+ })
+ documentFilePath: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The document status',
+ example: 'CREATED',
+ })
+ status: DocumentContractStatus;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The volunteer id',
+ example: '525dcdf9-4117-443e-a0c3-bf652cdc5c1b',
+ })
+ volunteerId: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The volunteer name',
+ example: 'John Doe',
+ })
+ volunteerName: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The organization id',
+ example: '525dcdf9-4117-443e-a0c3-bf652cdc5c1b',
+ })
+ organizationId: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The organization name',
+ example: 'John Doe',
+ })
+ organizationName: string;
+}
diff --git a/backend/src/migrations/1725878984294-DDLDocumentContractsSignature.ts b/backend/src/migrations/1725878984294-DDLDocumentContractsSignature.ts
new file mode 100644
index 000000000..3ff77e209
--- /dev/null
+++ b/backend/src/migrations/1725878984294-DDLDocumentContractsSignature.ts
@@ -0,0 +1,135 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class DDLDocumentContractsSignature1725878984294
+ implements MigrationInterface
+{
+ name = 'DDLDocumentContractsSignature1725878984294';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `CREATE TABLE "document_signature" ("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(), "signature" text NOT NULL, "user_id" uuid NOT NULL, CONSTRAINT "PK_36de69dee9bd822839c659c60a5" PRIMARY KEY ("id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_8c0295ec75967828329ef52ac3" ON "document_signature" ("created_on") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "document_contract" ("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(), "status" "public"."document_contract_status_enum" NOT NULL DEFAULT 'CREATED', "document_number" text NOT NULL, "document_date" date NOT NULL, "document_start_date" date NOT NULL, "document_end_date" date NOT NULL, "file_path" text, "volunteer_data" jsonb NOT NULL, "volunteer_tutor_data" jsonb, "volunteer_id" uuid NOT NULL, "organization_id" uuid NOT NULL, "document_template_id" uuid, "created_by_admin_id" uuid NOT NULL, "ngo_legal_representative_signature_id" uuid, "volunteer_signature_id" uuid, "tutor_signature_id" uuid, CONSTRAINT "PK_bc0002326db7d928c061fc90953" PRIMARY KEY ("id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_0bc738c91555e8a0ef9836de02" ON "document_contract" ("created_on") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "document_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_0e9c5bda0dd75f3bde7ae176c62" PRIMARY KEY ("id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_6dfe1cd0474df5d7d716bf59f0" ON "document_template" ("created_on") `,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_signature" ADD CONSTRAINT "FK_6e59057f55354293bf9f5e0a8df" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD CONSTRAINT "FK_7617b71c917deb25a66df28843d" FOREIGN KEY ("volunteer_id") REFERENCES "volunteer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD CONSTRAINT "FK_8a7ea2aea4dc1cf32f367850602" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD CONSTRAINT "FK_8167fbb2efdbc38871cf081515a" FOREIGN KEY ("document_template_id") REFERENCES "document_template"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD CONSTRAINT "FK_8de249c663986ee044dbef54fac" FOREIGN KEY ("created_by_admin_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD CONSTRAINT "FK_24ce6d12fac4a65325e44396e47" FOREIGN KEY ("ngo_legal_representative_signature_id") REFERENCES "document_signature"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD CONSTRAINT "FK_e82ad9df7db32f7c2d9034bba5f" FOREIGN KEY ("volunteer_signature_id") REFERENCES "document_signature"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD CONSTRAINT "FK_8586b2b6c023b4a93d301363004" FOREIGN KEY ("tutor_signature_id") REFERENCES "document_signature"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_template" ADD CONSTRAINT "FK_5b878af38db8ff501cbba07d97b" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_template" ADD CONSTRAINT "FK_efd8efceb4027c6e48af499e005" FOREIGN KEY ("created_by_admin_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(`CREATE VIEW "DocumentContractListView" AS
+ SELECT
+ document_contract.id as document_id,
+ document_contract.document_number as document_number,
+ document_contract.status as status,
+ document_contract.document_start_date as document_start_date,
+ document_contract.document_end_date as document_end_date,
+ document_contract.file_path AS document_file_path,
+ organization.id AS organization_id,
+ organization.name AS organization_name,
+ volunteer.id AS volunteer_id,
+ "user"."name" AS volunteer_name
+ FROM
+ document_contract
+ JOIN volunteer ON document_contract.volunteer_id = volunteer.id
+ JOIN "user" ON "user".id = volunteer.user_id
+ JOIN organization ON document_contract.organization_id = organization.id
+ `);
+ await queryRunner.query(
+ `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
+ [
+ 'public',
+ 'VIEW',
+ 'DocumentContractListView',
+ 'SELECT\n document_contract.id as document_id,\n document_contract.document_number as document_number,\n document_contract.status as status,\n document_contract.document_start_date as document_start_date,\n document_contract.document_end_date as document_end_date,\n document_contract.file_path AS document_file_path,\n organization.id AS organization_id,\n organization.name AS organization_name,\n volunteer.id AS volunteer_id,\n "user"."name" AS volunteer_name\n FROM\n document_contract\n JOIN volunteer ON document_contract.volunteer_id = volunteer.id\n JOIN "user" ON "user".id = volunteer.user_id\n JOIN organization ON document_contract.organization_id = organization.id',
+ ],
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
+ ['VIEW', 'DocumentContractListView', 'public'],
+ );
+ await queryRunner.query(`DROP VIEW "DocumentContractListView"`);
+ await queryRunner.query(
+ `ALTER TABLE "document_template" DROP CONSTRAINT "FK_efd8efceb4027c6e48af499e005"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_template" DROP CONSTRAINT "FK_5b878af38db8ff501cbba07d97b"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP CONSTRAINT "FK_8586b2b6c023b4a93d301363004"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP CONSTRAINT "FK_e82ad9df7db32f7c2d9034bba5f"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP CONSTRAINT "FK_24ce6d12fac4a65325e44396e47"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP CONSTRAINT "FK_8de249c663986ee044dbef54fac"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP CONSTRAINT "FK_8167fbb2efdbc38871cf081515a"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP CONSTRAINT "FK_8a7ea2aea4dc1cf32f367850602"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP CONSTRAINT "FK_7617b71c917deb25a66df28843d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_signature" DROP CONSTRAINT "FK_6e59057f55354293bf9f5e0a8df"`,
+ );
+ await queryRunner.query(
+ `DROP INDEX "public"."IDX_6dfe1cd0474df5d7d716bf59f0"`,
+ );
+ await queryRunner.query(`DROP TABLE "document_template"`);
+ await queryRunner.query(
+ `DROP INDEX "public"."IDX_0bc738c91555e8a0ef9836de02"`,
+ );
+ await queryRunner.query(`DROP TABLE "document_contract"`);
+ await queryRunner.query(
+ `DROP INDEX "public"."IDX_8c0295ec75967828329ef52ac3"`,
+ );
+ await queryRunner.query(`DROP TABLE "document_signature"`);
+ }
+}
diff --git a/backend/src/modules/documents/documents.module.ts b/backend/src/modules/documents/documents.module.ts
index fa8bb2a2b..e170d753d 100644
--- a/backend/src/modules/documents/documents.module.ts
+++ b/backend/src/modules/documents/documents.module.ts
@@ -10,13 +10,23 @@ 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';
+import { DocumentContractEntity } from './entities/document-contract.entity';
+import { DocumentContractRepositoryService } from './repositories/document-contract.repository';
+import { SignatureRepositoryService } from './repositories/document-signature.repository';
+import { DocumentContractFacade } from './services/document-contract.facade';
+import { DocumentContractListViewEntity } from './entities/document-contract-list-view.entity';
+import { DocumentContractListViewRepository } from './repositories/document-contract-list-view.repository';
+import { DocumentSignatureEntity } from './entities/document-signature.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
+ DocumentContractListViewEntity,
TemplateEntity,
ContractEntity,
DocumentTemplateEntity,
+ DocumentContractEntity,
+ DocumentSignatureEntity,
]),
],
providers: [
@@ -24,10 +34,14 @@ import { DocumentTemplateEntity } from './entities/document-template.entity';
TemplateRepositoryService,
ContractRepositoryService,
DocumentTemplateRepositoryService,
+ DocumentContractRepositoryService,
+ SignatureRepositoryService,
+ DocumentContractListViewRepository,
// Facades
TemplateFacade,
ContractFacade,
DocumentTemplateFacade,
+ DocumentContractFacade,
// Services
PDFGenerator,
],
@@ -37,6 +51,7 @@ import { DocumentTemplateEntity } from './entities/document-template.entity';
ContractFacade,
PDFGenerator,
DocumentTemplateFacade,
+ DocumentContractFacade,
],
})
export class DocumentsModule {}
diff --git a/backend/src/modules/documents/entities/document-contract-list-view.entity.ts b/backend/src/modules/documents/entities/document-contract-list-view.entity.ts
new file mode 100644
index 000000000..7233e5735
--- /dev/null
+++ b/backend/src/modules/documents/entities/document-contract-list-view.entity.ts
@@ -0,0 +1,54 @@
+import { ViewColumn, ViewEntity } from 'typeorm';
+import { DocumentContractStatus } from '../enums/contract-status.enum';
+
+@ViewEntity('DocumentContractListView', {
+ expression: `
+ SELECT
+ document_contract.id as document_id,
+ document_contract.document_number as document_number,
+ document_contract.status as status,
+ document_contract.document_start_date as document_start_date,
+ document_contract.document_end_date as document_end_date,
+ document_contract.file_path AS document_file_path,
+ organization.id AS organization_id,
+ organization.name AS organization_name,
+ volunteer.id AS volunteer_id,
+ "user"."name" AS volunteer_name
+ FROM
+ document_contract
+ JOIN volunteer ON document_contract.volunteer_id = volunteer.id
+ JOIN "user" ON "user".id = volunteer.user_id
+ JOIN organization ON document_contract.organization_id = organization.id
+ `,
+})
+export class DocumentContractListViewEntity {
+ @ViewColumn({ name: 'document_id' })
+ documentId: string;
+
+ @ViewColumn({ name: 'document_number' })
+ documentNumber: string;
+
+ @ViewColumn({ name: 'document_start_date' })
+ documentStartDate: Date;
+
+ @ViewColumn({ name: 'document_end_date' })
+ documentEndDate: Date;
+
+ @ViewColumn({ name: 'document_file_path' })
+ documentFilePath: string;
+
+ @ViewColumn({ name: 'status' })
+ status: DocumentContractStatus;
+
+ @ViewColumn({ name: 'volunteer_id' })
+ volunteerId: string;
+
+ @ViewColumn({ name: 'volunteer_name' })
+ volunteerName: string;
+
+ @ViewColumn({ name: 'organization_id' })
+ organizationId: string;
+
+ @ViewColumn({ name: 'organization_name' })
+ organizationName: string;
+}
diff --git a/backend/src/modules/documents/entities/document-contract.entity.ts b/backend/src/modules/documents/entities/document-contract.entity.ts
new file mode 100644
index 000000000..4fdf1ebf3
--- /dev/null
+++ b/backend/src/modules/documents/entities/document-contract.entity.ts
@@ -0,0 +1,161 @@
+import {
+ Column,
+ Entity,
+ JoinColumn,
+ ManyToOne,
+ PrimaryGeneratedColumn,
+} from 'typeorm';
+import { DocumentContractStatus } from '../enums/contract-status.enum';
+import { OrganizationEntity } from 'src/modules/organization/entities/organization.entity';
+import { DocumentTemplateEntity } from './document-template.entity';
+import { AdminUserEntity } from 'src/modules/user/entities/user.entity';
+import { VolunteerEntity } from 'src/modules/volunteer/entities/volunteer.entity';
+import { BaseEntity } from 'src/infrastructure/base/base-entity';
+import { PersonalData } from '../models/document-contract.model';
+import { DocumentSignatureEntity } from './document-signature.entity';
+
+@Entity({ name: 'document_contract' })
+export class DocumentContractEntity extends BaseEntity {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({
+ type: 'enum',
+ enum: DocumentContractStatus,
+ name: 'status',
+ default: DocumentContractStatus.CREATED,
+ })
+ status: DocumentContractStatus;
+
+ @Column({ type: 'text', name: 'document_number' })
+ documentNumber: string;
+
+ @Column({ type: 'date', name: 'document_date' })
+ documentDate: Date;
+
+ @Column({ type: 'date', name: 'document_start_date' })
+ documentStartDate: Date;
+
+ @Column({ type: 'date', name: 'document_end_date' })
+ documentEndDate: Date;
+
+ @Column({ type: 'text', name: 'file_path', nullable: true })
+ filePath: string;
+
+ // ==================== VOLUNTEER RELATION =================================
+
+ @Column({ type: 'jsonb', name: 'volunteer_data', nullable: false })
+ volunteerData: PersonalData;
+
+ @Column({ type: 'jsonb', name: 'volunteer_tutor_data', nullable: true })
+ volunteerTutorData: PersonalData;
+
+ @Column({
+ type: 'varchar',
+ name: 'volunteer_id',
+ })
+ volunteerId: string;
+
+ @ManyToOne(() => VolunteerEntity)
+ @JoinColumn({ name: 'volunteer_id' })
+ volunteer: VolunteerEntity;
+
+ // ==================== ORGANIZATION RELATION =================================
+ @Column({
+ type: 'varchar',
+ name: 'organization_id',
+ })
+ organizationId: string;
+
+ @ManyToOne(() => OrganizationEntity)
+ @JoinColumn({ name: 'organization_id' })
+ organization: OrganizationEntity;
+
+ // ==================== TEMPLATE RELATION =================================
+
+ @Column({
+ type: 'varchar',
+ nullable: true,
+ name: 'document_template_id',
+ })
+ documentTemplateId: string;
+
+ @ManyToOne(() => DocumentTemplateEntity)
+ @JoinColumn({ name: 'document_template_id' })
+ documentTemplate: DocumentTemplateEntity;
+
+ // ==================== CONTRACT CREATED BY =================================
+
+ @Column({
+ type: 'varchar',
+ name: 'created_by_admin_id',
+ })
+ createdByAdminId: string;
+
+ @ManyToOne(() => AdminUserEntity)
+ @JoinColumn({ name: 'created_by_admin_id' })
+ createdByAdmin: AdminUserEntity;
+
+ // ======================== SIGNATURES =================================
+ @Column({
+ type: 'varchar',
+ name: 'ngo_legal_representative_signature_id',
+ nullable: true,
+ })
+ ngoLegalRepresentativeSignatureId: string;
+
+ @ManyToOne(() => DocumentSignatureEntity)
+ @JoinColumn({ name: 'ngo_legal_representative_signature_id' })
+ ngoLegalRepresentativeSignature: DocumentSignatureEntity;
+
+ @Column({
+ type: 'varchar',
+ name: 'volunteer_signature_id',
+ nullable: true,
+ })
+ volunteerSignatureId: string;
+
+ @ManyToOne(() => DocumentSignatureEntity)
+ @JoinColumn({ name: 'volunteer_signature_id' })
+ volunteerSignature: DocumentSignatureEntity;
+
+ @Column({
+ type: 'varchar',
+ name: 'tutor_signature_id',
+ nullable: true,
+ })
+ tutorSignatureId: string;
+
+ @ManyToOne(() => DocumentSignatureEntity)
+ @JoinColumn({ name: 'tutor_signature_id' })
+ tutorSignature: DocumentSignatureEntity;
+
+ // // ==================== APPROVAL =================================
+
+ // TODO: instead of keeping here the approval/rejection/signatures we can keep them in ActionsArchive
+
+ // @Column({ type: 'timestamptz', name: 'approved_on', nullable: true })
+ // approvedOn: Date;
+
+ // @Column({ type: 'string', name: 'approved_by', nullable: true })
+ // approvedById: string;
+
+ // @ManyToOne(() => AdminUserEntity)
+ // @JoinColumn({ name: 'approved_by' })
+ // approvedBy: AdminUserEntity;
+
+ // // ==================== REJECTION =================================
+
+ // @Column({ type: 'text', name: 'rejection_reason', nullable: true })
+ // rejectionReason: string;
+
+ // @Column({ type: 'timestamptz', name: 'rejected_on', nullable: true })
+ // rejectedOn: Date;
+
+ // @Column({ type: 'string', name: 'rejected_by', nullable: true })
+ // rejectedById: string;
+
+ // @ManyToOne(() => AdminUserEntity)
+ // @JoinColumn({ name: 'rejected_by' })
+ // rejectedBy: AdminUserEntity;
+}
diff --git a/backend/src/modules/documents/entities/document-signature.entity.ts b/backend/src/modules/documents/entities/document-signature.entity.ts
new file mode 100644
index 000000000..c1bc05aec
--- /dev/null
+++ b/backend/src/modules/documents/entities/document-signature.entity.ts
@@ -0,0 +1,25 @@
+import { BaseEntity } from 'src/infrastructure/base/base-entity';
+import { UserEntity } from 'src/modules/user/entities/user.entity';
+import {
+ Column,
+ Entity,
+ JoinColumn,
+ ManyToOne,
+ PrimaryGeneratedColumn,
+} from 'typeorm';
+
+@Entity({ name: 'document_signature' })
+export class DocumentSignatureEntity extends BaseEntity {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({ type: 'text', name: 'signature' })
+ signature: string;
+
+ @Column({ type: 'string', name: 'user_id' })
+ userId: string;
+
+ @ManyToOne(() => UserEntity)
+ @JoinColumn({ name: 'user_id' })
+ user: UserEntity;
+}
diff --git a/backend/src/modules/documents/entities/document-template.entity.ts b/backend/src/modules/documents/entities/document-template.entity.ts
index 179883c39..2f7ff1f3f 100644
--- a/backend/src/modules/documents/entities/document-template.entity.ts
+++ b/backend/src/modules/documents/entities/document-template.entity.ts
@@ -6,10 +6,12 @@ import {
Entity,
JoinColumn,
ManyToOne,
+ OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
+import { DocumentContractEntity } from './document-contract.entity';
-@Entity({ name: 'documents_template' })
+@Entity({ name: 'document_template' })
export class DocumentTemplateEntity extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -44,8 +46,12 @@ export class DocumentTemplateEntity extends BaseEntity {
@JoinColumn({ name: 'created_by_admin_id' })
createdByAdmin: AdminUserEntity;
- // @OneToMany(() => ContractEntity, (contract) => contract.template, {
- // onDelete: 'SET NULL',
- // })
- // contracts: ContractEntity[];
+ @OneToMany(
+ () => DocumentContractEntity,
+ (contract) => contract.documentTemplate,
+ {
+ onDelete: 'SET NULL',
+ },
+ )
+ contracts: DocumentContractEntity[];
}
diff --git a/backend/src/modules/documents/enums/contract-status.enum.ts b/backend/src/modules/documents/enums/contract-status.enum.ts
index a29af5fe9..04ac3452d 100644
--- a/backend/src/modules/documents/enums/contract-status.enum.ts
+++ b/backend/src/modules/documents/enums/contract-status.enum.ts
@@ -4,3 +4,15 @@ export enum ContractStatus {
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
}
+
+export enum DocumentContractStatus {
+ CREATED = 'CREATED', // just created, not sent to volunteer
+ SCHEDULED = 'SCHEDULED', // se va trimite la o data setata intr-un CRON si isi va schimba statusul in PENDING_VOLUNTEER
+ PENDING_VOLUNTEER_SIGNATURE = 'PENDING_VOLUNTEER_SIGNATURE', // fost creat de Admin si trimis catre Voluntar
+ PENDING_APPROVAL_NGO = 'PENDING_APPROVAL_NGO', // a fost semnat de voluntar si trimis la ONG pentru verificare
+ PENDING_NGO_REPRESENTATIVE_SIGNATURE = 'PENDING_NGO_REPRESENTATIVE_SIGNATURE', // a fost aprobat de catre ONG si trimis catre reprezentantul legal al ONG-ului pentru semnare
+ APPROVED = 'APPROVED', // a fost aprobat de ambele parti
+ REJECTED_VOLUNTEER = 'REJECTED_VOLUNTEER', // a fost rejected de catre Voluntar
+ REJECTED_NGO = 'REJECTED_NGO', // a fost rejected de catre NGO
+ ACTION_EXPIRED = 'ACTION_EXPIRED', // a expirat dupa 30 zile de la generare daca nu a primit raspuns de la voluntar
+}
diff --git a/backend/src/modules/documents/models/document-contract-list-view.model.ts b/backend/src/modules/documents/models/document-contract-list-view.model.ts
new file mode 100644
index 000000000..f5509a08f
--- /dev/null
+++ b/backend/src/modules/documents/models/document-contract-list-view.model.ts
@@ -0,0 +1,33 @@
+import { IBasePaginationFilterModel } from 'src/infrastructure/base/base-pagination-filter.model';
+import { DocumentContractStatus } from '../enums/contract-status.enum';
+import { DocumentContractListViewEntity } from '../entities/document-contract-list-view.entity';
+
+export interface IDocumentContractListViewModel {
+ documentId: string;
+ documentNumber: string;
+ documentStartDate: Date;
+ documentEndDate: Date;
+ documentFilePath: string;
+ status: DocumentContractStatus;
+ volunteerId: string;
+ volunteerName: string;
+ organizationId: string;
+ organizationName: string;
+}
+
+export type FindManyDocumentContractListViewOptions =
+ IBasePaginationFilterModel & {
+ organizationId: string;
+ volunteerId?: string;
+ status?: DocumentContractStatus;
+ };
+
+export class DocumentContractListViewTransformer {
+ static fromEntity(
+ entity: DocumentContractListViewEntity,
+ ): IDocumentContractListViewModel {
+ return {
+ ...entity,
+ };
+ }
+}
diff --git a/backend/src/modules/documents/models/document-contract.model.ts b/backend/src/modules/documents/models/document-contract.model.ts
new file mode 100644
index 000000000..62ce19776
--- /dev/null
+++ b/backend/src/modules/documents/models/document-contract.model.ts
@@ -0,0 +1,141 @@
+import { IBaseModel } from 'src/common/interfaces/base.model';
+import { DocumentContractStatus } from '../enums/contract-status.enum';
+import { DocumentContractEntity } from '../entities/document-contract.entity';
+import {
+ DocumentTemplateTransformer,
+ IDocumentTemplateModel,
+} from './document-template.model';
+import {
+ AdminUserTransformer,
+ IAdminUserModel,
+} from 'src/modules/user/models/admin-user.model';
+import {
+ IVolunteerModel,
+ VolunteerModelTransformer,
+} from 'src/modules/volunteer/model/volunteer.model';
+
+// TODO: Change this with the existent IUserPersonalDataModel after is updated
+export interface PersonalData {
+ CNP: string;
+ name: string;
+ address: string;
+ identityDocumentSeries: string;
+ identityDocumentNumber: string;
+ identityDocumentIssuedBy: string;
+ identityDocumentIssuedDate: Date;
+}
+
+export interface IDocumentContractModel extends IBaseModel {
+ id: string;
+ status: DocumentContractStatus;
+
+ documentNumber: string;
+ documentDate: Date;
+ documentStartDate: Date;
+ documentEndDate: Date;
+
+ organizationId: string;
+
+ // Template
+ documentTemplateId: string;
+ documentTemplate: IDocumentTemplateModel; // TODO: we don't always need all the data here... but id and name, hmm...
+
+ // Created By
+ createdByAdminId: string;
+ createdByAdmin: IAdminUserModel;
+
+ // Volunteer
+ volunteerId: string;
+ volunteer: IVolunteerModel;
+
+ volunteerData: PersonalData; // TODO: le tragem din user in usecase-ul de creare contract si raman ca snapshoot
+ volunteerTutorData?: PersonalData; // TODO: le tragem din user in usecase-ul de creare contract si raman ca snapshoot
+
+ filePath?: string;
+
+ ngoLegalRepresentativeSignatureId?: string;
+ volunteerSignatureId?: string;
+ tutorSignatureId?: string;
+}
+
+export type CreateDocumentContractOptions = {
+ status: DocumentContractStatus;
+
+ documentNumber: string;
+ documentDate: Date;
+ documentStartDate: Date;
+ documentEndDate: Date;
+
+ volunteerData: PersonalData;
+ volunteerTutorData?: PersonalData;
+
+ volunteerId: string;
+ organizationId: string;
+ documentTemplateId: string;
+ createdByAdminId: string;
+};
+
+export type UpdateDocumentContractOptions = {
+ status?: DocumentContractStatus;
+ filePath?: string;
+ ngoLegalRepresentativeSignatureId?: string;
+ volunteerSignatureId?: string;
+ tutorSignatureId?: string;
+};
+
+export type FindOneDocumentContractOptions = Pick;
+
+export class DocumentContractTransformer {
+ static fromEntity(entity: DocumentContractEntity): IDocumentContractModel {
+ if (!entity) {
+ return null;
+ }
+
+ return {
+ id: entity.id,
+ status: entity.status,
+ documentNumber: entity.documentNumber,
+ documentDate: entity.documentDate,
+ documentStartDate: entity.documentStartDate,
+ documentEndDate: entity.documentEndDate,
+
+ organizationId: entity.organizationId,
+
+ // Volunteer
+ volunteerId: entity.volunteerId,
+ volunteer: VolunteerModelTransformer.fromEntity(entity.volunteer),
+ volunteerData: entity.volunteerData,
+ volunteerTutorData: entity.volunteerTutorData,
+ // Template
+ documentTemplateId: entity.documentTemplateId,
+ documentTemplate: DocumentTemplateTransformer.fromEntity(
+ entity.documentTemplate,
+ ),
+ // CreatedBy
+ createdByAdminId: entity.createdByAdminId,
+ createdByAdmin: AdminUserTransformer.fromEntity(entity.createdByAdmin),
+
+ createdOn: entity.createdOn,
+ updatedOn: entity.updatedOn,
+ };
+ }
+
+ static createDocumentContractToEntity(
+ model: CreateDocumentContractOptions,
+ ): DocumentContractEntity {
+ const entity = new DocumentContractEntity();
+ entity.status = model.status;
+ entity.documentNumber = model.documentNumber;
+ entity.documentDate = model.documentDate;
+ entity.documentStartDate = model.documentStartDate;
+ entity.documentEndDate = model.documentEndDate;
+ entity.volunteerId = model.volunteerId;
+ entity.volunteerData = model.volunteerData;
+ entity.volunteerTutorData = model.volunteerTutorData;
+ entity.organizationId = model.organizationId;
+ entity.documentTemplateId = model.documentTemplateId;
+ entity.createdByAdminId = model.createdByAdminId;
+
+ return entity;
+ }
+}
diff --git a/backend/src/modules/documents/models/document-signature.model.ts b/backend/src/modules/documents/models/document-signature.model.ts
new file mode 100644
index 000000000..92807927a
--- /dev/null
+++ b/backend/src/modules/documents/models/document-signature.model.ts
@@ -0,0 +1,21 @@
+import { IBaseModel } from 'src/common/interfaces/base.model';
+
+export interface SignatureModel extends IBaseModel {
+ id: string;
+
+ signature: string;
+
+ userId: string;
+
+ createdOn: Date;
+ updatedOn: Date;
+}
+
+export type CreateSignatureOptions = Pick<
+ SignatureModel,
+ 'signature' | 'userId'
+>;
+
+export type FindOneSignatureOptions = Partial<
+ Pick
+>;
diff --git a/backend/src/modules/documents/models/document-template.model.ts b/backend/src/modules/documents/models/document-template.model.ts
index 628439aac..6d305eaef 100644
--- a/backend/src/modules/documents/models/document-template.model.ts
+++ b/backend/src/modules/documents/models/document-template.model.ts
@@ -29,7 +29,9 @@ export type CreateDocumentTemplateOptions = Omit<
'id' | 'createdOn' | 'updatedOn' | 'createdByAdmin'
> & { createdByAdminId: string };
-export type FindOneDocumentTemplateOptions = Pick;
+export type FindOneDocumentTemplateOptions = Partial<
+ Pick
+>;
export class DocumentTemplateTransformer {
static fromEntity(entity: DocumentTemplateEntity): IDocumentTemplateModel {
diff --git a/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts b/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts
new file mode 100644
index 000000000..23aefd6db
--- /dev/null
+++ b/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts
@@ -0,0 +1,86 @@
+import {
+ Pagination,
+ RepositoryWithPagination,
+} from 'src/infrastructure/base/repository-with-pagination.class';
+import { DocumentContractListViewEntity } from '../entities/document-contract-list-view.entity';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { Injectable } from '@nestjs/common';
+import {
+ DocumentContractListViewTransformer,
+ FindManyDocumentContractListViewOptions,
+ IDocumentContractListViewModel,
+} from '../models/document-contract-list-view.model';
+import { OrderDirection } from 'src/common/enums/order-direction.enum';
+
+@Injectable()
+export class DocumentContractListViewRepository extends RepositoryWithPagination {
+ constructor(
+ @InjectRepository(DocumentContractListViewEntity)
+ private readonly documentContractListViewRepository: Repository,
+ ) {
+ super(documentContractListViewRepository);
+
+ // this.findMany({
+ // limit: 10,
+ // page: 1,
+ // organizationId: '7f005461-07c3-4693-a85d-40d31db43a4c',
+ // status: DocumentContractStatus.APPROVED,
+ // }).then(console.log);
+ }
+
+ async findMany(
+ findOptions: FindManyDocumentContractListViewOptions,
+ ): Promise> {
+ const {
+ orderBy,
+ orderDirection,
+ search,
+ limit,
+ page,
+
+ organizationId,
+ volunteerId,
+ status,
+ } = findOptions;
+
+ const query = this.documentContractListViewRepository
+ .createQueryBuilder('documentContractListView')
+ .where('documentContractListView.organizationId = :organizationId', {
+ organizationId,
+ })
+ .orderBy(
+ this.buildOrderByQuery(
+ orderBy || 'documentNumber',
+ 'documentContractListView',
+ ),
+ orderDirection || OrderDirection.ASC,
+ );
+
+ if (volunteerId) {
+ query.andWhere('documentContractListView.volunteerId = :volunteerId', {
+ volunteerId,
+ });
+ }
+
+ if (status) {
+ query.andWhere('documentContractListView.status = :status', { status });
+ }
+
+ if (search) {
+ query.andWhere(
+ this.buildBracketSearchQuery(
+ ['documentContractListView.documentNumber', 'user.name'],
+ search,
+ ),
+ );
+ }
+
+ return this.paginateQuery(
+ query,
+ limit,
+ page,
+ DocumentContractListViewTransformer.fromEntity,
+ );
+ }
+}
diff --git a/backend/src/modules/documents/repositories/document-contract.repository.ts b/backend/src/modules/documents/repositories/document-contract.repository.ts
new file mode 100644
index 000000000..97a06cc87
--- /dev/null
+++ b/backend/src/modules/documents/repositories/document-contract.repository.ts
@@ -0,0 +1,72 @@
+import { Injectable } from '@nestjs/common';
+import { DocumentContractEntity } from '../entities/document-contract.entity';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { RepositoryWithPagination } from 'src/infrastructure/base/repository-with-pagination.class';
+import {
+ CreateDocumentContractOptions,
+ DocumentContractTransformer,
+ FindOneDocumentContractOptions,
+ IDocumentContractModel,
+ UpdateDocumentContractOptions,
+} from '../models/document-contract.model';
+
+@Injectable()
+export class DocumentContractRepositoryService extends RepositoryWithPagination {
+ // TODO: implement IDocumentContractRepository
+ constructor(
+ @InjectRepository(DocumentContractEntity)
+ private readonly documentContractRepository: Repository,
+ ) {
+ super(documentContractRepository);
+ }
+
+ async create(
+ newDocumentContract: CreateDocumentContractOptions,
+ ): Promise {
+ const documentContract = await this.documentContractRepository.save(
+ DocumentContractTransformer.createDocumentContractToEntity(
+ newDocumentContract,
+ ),
+ );
+
+ return documentContract.id;
+ }
+
+ async findOne(
+ options: FindOneDocumentContractOptions,
+ ): Promise {
+ const documentContract = await this.documentContractRepository.findOne({
+ where: options,
+ });
+
+ return DocumentContractTransformer.fromEntity(documentContract);
+ }
+
+ async update(
+ id: string,
+ updates: UpdateDocumentContractOptions,
+ ): Promise {
+ const documentContract = await this.documentContractRepository.preload({
+ id,
+ ...updates,
+ });
+
+ await this.documentContractRepository.save(documentContract);
+
+ return this.findOne({ id });
+ }
+
+ async delete(id: string): Promise {
+ const documentContract = await this.documentContractRepository.find({
+ where: { id },
+ });
+
+ if (documentContract) {
+ await this.documentContractRepository.remove(documentContract);
+ return id;
+ }
+
+ return null;
+ }
+}
diff --git a/backend/src/modules/documents/repositories/document-signature.repository.ts b/backend/src/modules/documents/repositories/document-signature.repository.ts
new file mode 100644
index 000000000..6fd9ca9e2
--- /dev/null
+++ b/backend/src/modules/documents/repositories/document-signature.repository.ts
@@ -0,0 +1,32 @@
+import { DocumentSignatureEntity } from '../entities/document-signature.entity';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import {
+ CreateSignatureOptions,
+ FindOneSignatureOptions,
+} from '../models/document-signature.model';
+import { Injectable } from '@nestjs/common';
+
+@Injectable()
+export class SignatureRepositoryService {
+ constructor(
+ @InjectRepository(DocumentSignatureEntity)
+ private readonly signatureRepository: Repository,
+ ) {}
+
+ async create(
+ newSignature: CreateSignatureOptions,
+ ): Promise {
+ const signature = await this.signatureRepository.save(newSignature);
+ return signature;
+ }
+
+ async findOne(
+ options: FindOneSignatureOptions,
+ ): Promise {
+ const signature = await this.signatureRepository.findOne({
+ where: options,
+ });
+ return signature;
+ }
+}
diff --git a/backend/src/modules/documents/services/document-contract.facade.ts b/backend/src/modules/documents/services/document-contract.facade.ts
new file mode 100644
index 000000000..63a46b775
--- /dev/null
+++ b/backend/src/modules/documents/services/document-contract.facade.ts
@@ -0,0 +1,51 @@
+import { Injectable } from '@nestjs/common';
+import { DocumentContractRepositoryService } from '../repositories/document-contract.repository';
+import {
+ CreateDocumentContractOptions,
+ IDocumentContractModel,
+ FindOneDocumentContractOptions,
+ UpdateDocumentContractOptions,
+} from '../models/document-contract.model';
+import { DocumentContractListViewRepository } from '../repositories/document-contract-list-view.repository';
+import {
+ FindManyDocumentContractListViewOptions,
+ IDocumentContractListViewModel,
+} from '../models/document-contract-list-view.model';
+import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
+
+@Injectable()
+export class DocumentContractFacade {
+ constructor(
+ private readonly documentContractRepository: DocumentContractRepositoryService,
+ private readonly documentContractListViewRepository: DocumentContractListViewRepository,
+ ) {}
+
+ async create(
+ newDocumentContract: CreateDocumentContractOptions,
+ ): Promise {
+ return this.documentContractRepository.create(newDocumentContract);
+ }
+
+ async findOne(
+ options: FindOneDocumentContractOptions,
+ ): Promise {
+ return this.documentContractRepository.findOne(options);
+ }
+
+ async findMany(
+ options: FindManyDocumentContractListViewOptions,
+ ): Promise> {
+ return this.documentContractListViewRepository.findMany(options);
+ }
+
+ async update(
+ id: string,
+ updates: UpdateDocumentContractOptions,
+ ): Promise {
+ return this.documentContractRepository.update(id, updates);
+ }
+
+ async delete(id: string): Promise {
+ return this.documentContractRepository.delete(id);
+ }
+}
diff --git a/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts b/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts
new file mode 100644
index 000000000..c1f6926ff
--- /dev/null
+++ b/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts
@@ -0,0 +1,188 @@
+import { Injectable } from '@nestjs/common';
+import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface';
+import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service';
+import { CreateDocumentContractOptions } from 'src/modules/documents/models/document-contract.model';
+import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade';
+import { DocumentTemplateFacade } from 'src/modules/documents/services/document-template.facade';
+import { IUserPersonalDataModel } from 'src/modules/user/models/user-personal-data.model';
+import { VolunteerStatus } from 'src/modules/volunteer/enums/volunteer-status.enum';
+import { VolunteerExceptionMessages } from 'src/modules/volunteer/exceptions/volunteer.exceptions';
+import { IVolunteerModel } from 'src/modules/volunteer/model/volunteer.model';
+import { VolunteerFacade } from 'src/modules/volunteer/services/volunteer.facade';
+import { GetOrganizationUseCaseService } from 'src/usecases/organization/get-organization.usecase';
+import * as z from 'zod';
+
+@Injectable()
+export class CreateDocumentContractUsecase implements IUseCaseService {
+ constructor(
+ private readonly documentContractFacade: DocumentContractFacade,
+ private readonly documentTemplateFacade: DocumentTemplateFacade,
+ private readonly getOrganizationUsecase: GetOrganizationUseCaseService,
+ private readonly volunteerFacade: VolunteerFacade,
+ private readonly exceptionsService: ExceptionsService,
+ ) {
+ // this.execute({
+ // documentDate: new Date(),
+ // documentStartDate: new Date(),
+ // documentEndDate: new Date(),
+ // volunteerId: '1a53406f-263b-41bc-b60c-cb30a1805f1e',
+ // organizationId: '7f005461-07c3-4693-a85d-40d31db43a4c',
+ // documentTemplateId: 'bc3b7d74-686e-47b4-850a-b1de69574e28',
+ // createdByAdminId: '4db075bd-4095-432e-98bd-dc68b4599337',
+ // status: DocumentContractStatus.CREATED,
+ // });
+ }
+
+ public async execute(
+ newContract: Omit<
+ CreateDocumentContractOptions,
+ 'volunteerData' | 'volunteerTutorData'
+ >,
+ ): Promise {
+ // 1. check if the organization exists
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const organization = await this.getOrganizationUsecase.execute(
+ newContract.organizationId,
+ );
+
+ // 2. check if the volunteer exists
+ const volunteer = await this.checkVolunteerExists(
+ newContract.volunteerId,
+ newContract.organizationId,
+ );
+
+ //3. check if template exists
+ await this.checkTemplateExists(
+ newContract.documentTemplateId,
+ newContract.organizationId,
+ );
+
+ //TODO: 4. check if the contract number already exists
+
+ //TODO: 5. check if the volunteer has already a contract in that period
+
+ // 6. Extract volunteerData and volunteerTutorData from the user
+ const volunteerPersonalData = volunteer.user.userPersonalData;
+ await this.validateVolunteerPersonalData(volunteerPersonalData);
+ // TODO: 6.1. Extract volunteerTutorData from the user and validate it
+
+ const newContractOptions: CreateDocumentContractOptions = {
+ ...newContract,
+ volunteerData: {
+ name: volunteer.user.name,
+ CNP: '11321321312321 MISSING',
+ address: volunteerPersonalData.address,
+ identityDocumentSeries: volunteerPersonalData.identityDocumentSeries,
+ identityDocumentNumber: volunteerPersonalData.identityDocumentNumber,
+ // identityDocumentIssuedBy: volunteerPersonalData.identityDocumentIssuedBy,
+ // identityDocumentIssuedDate: volunteerPersonalData.identityDocumentIssuedDate,
+ identityDocumentIssuedBy: 'Missing',
+ identityDocumentIssuedDate: new Date(),
+ },
+ volunteerTutorData: {
+ CNP: '11321321312321',
+ address: 'Str. Unirii 59',
+ name: 'Test',
+ identityDocumentSeries: 'AB',
+ identityDocumentNumber: '1234567890',
+ identityDocumentIssuedBy: 'Comisaria',
+ identityDocumentIssuedDate: new Date(),
+ },
+ };
+
+ // 7. Create the contract
+ let contractId: string;
+ try {
+ contractId = await this.documentContractFacade.create(newContractOptions);
+ } catch (error) {
+ this.exceptionsService.internalServerErrorException({
+ message: 'Error creating contract',
+ code_error: 'ERROR_CREATING_CONTRACT', // TODO: create a new error code for this
+ });
+ }
+
+ // 8. Build the HTML with handlebars and set it to lambda to Create the PDF
+
+ // 9. Send notification to the volunteer to sign the contract if the status is PENDING_VOLUNTEER_SIGNATURE
+
+ // 10. Track event
+
+ return contractId;
+ }
+
+ private async validateVolunteerPersonalData(
+ volunteerPersonalData: IUserPersonalDataModel,
+ ): Promise {
+ const personalDataSchema = z.object({
+ CNP: z.string().length(13, 'CNP must be 13 digits'),
+ address: z.string().min(1, 'Address is required'),
+ identityDocumentSeries: z
+ .string()
+ .min(2, 'Identity document series is required'),
+ identityDocumentNumber: z
+ .string()
+ .min(1, 'Identity document number is required'),
+ identityDocumentIssuedBy: z
+ .string()
+ .min(1, 'Identity document issuer is required'),
+ identityDocumentIssuedDate: z
+ .date()
+ .max(new Date(), 'Issue date cannot be in the future'),
+ });
+
+ try {
+ personalDataSchema.parse(volunteerPersonalData);
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const invalidFields = error.issues.map((issue) => ({
+ field: issue.path.join('.'),
+ message: issue.message,
+ }));
+
+ this.exceptionsService.badRequestException({
+ message: `Invalid personal data ${JSON.stringify(invalidFields)}`,
+ code_error: 'INVALID_PERSONAL_DATA', // TODO: create a new error code for this
+ });
+ } else {
+ throw error; // Re-throw unexpected errors
+ }
+ }
+ }
+
+ private async checkVolunteerExists(
+ volunteerId: string,
+ organizationId: string,
+ ): Promise {
+ const volunteer = await this.volunteerFacade.find({
+ id: volunteerId,
+ organizationId: organizationId,
+ status: VolunteerStatus.ACTIVE,
+ });
+
+ if (!volunteer) {
+ this.exceptionsService.notFoundException(
+ VolunteerExceptionMessages.VOLUNTEER_001,
+ );
+ }
+
+ return volunteer;
+ }
+
+ private async checkTemplateExists(
+ documentTemplateId: string,
+ organizationId: string,
+ ): Promise {
+ const template = await this.documentTemplateFacade.findOne({
+ id: documentTemplateId,
+ organizationId: organizationId,
+ });
+
+ if (!template) {
+ this.exceptionsService.notFoundException({
+ // TODO update this exception
+ message: 'Template not found',
+ code_error: 'TEMPLATE_NOT_FOUND',
+ });
+ }
+ }
+}
diff --git a/backend/src/usecases/documents/create-document-template.usecase.ts b/backend/src/usecases/documents/new_contracts/create-document-template.usecase.ts
similarity index 100%
rename from backend/src/usecases/documents/create-document-template.usecase.ts
rename to backend/src/usecases/documents/new_contracts/create-document-template.usecase.ts
diff --git a/backend/src/usecases/documents/new_contracts/get-many-document-contracts.usecase.ts b/backend/src/usecases/documents/new_contracts/get-many-document-contracts.usecase.ts
new file mode 100644
index 000000000..10e3d9312
--- /dev/null
+++ b/backend/src/usecases/documents/new_contracts/get-many-document-contracts.usecase.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@nestjs/common';
+import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface';
+import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
+import {
+ FindManyDocumentContractListViewOptions,
+ IDocumentContractListViewModel,
+} from 'src/modules/documents/models/document-contract-list-view.model';
+import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade';
+
+@Injectable()
+export class GetManyDocumentContractsUsecase
+ implements IUseCaseService>
+{
+ constructor(
+ private readonly documentContractFacade: DocumentContractFacade,
+ ) {}
+
+ public async execute(
+ findOptions: FindManyDocumentContractListViewOptions,
+ ): Promise> {
+ return this.documentContractFacade.findMany(findOptions);
+ }
+}
diff --git a/backend/src/usecases/documents/get-one-document-template.usecase.ts b/backend/src/usecases/documents/new_contracts/get-one-document-template.usecase.ts
similarity index 79%
rename from backend/src/usecases/documents/get-one-document-template.usecase.ts
rename to backend/src/usecases/documents/new_contracts/get-one-document-template.usecase.ts
index 50143b8f4..20a5a8678 100644
--- a/backend/src/usecases/documents/get-one-document-template.usecase.ts
+++ b/backend/src/usecases/documents/new_contracts/get-one-document-template.usecase.ts
@@ -21,11 +21,12 @@ export class GetOneDocumentTemplateUseCase
findOptions: FindOneDocumentTemplateOptions,
organizationId: string,
): Promise {
- const template = await this.documentTemplateFacade.findOne(findOptions);
+ const template = await this.documentTemplateFacade.findOne({
+ ...findOptions,
+ organizationId,
+ });
- // Check if the provided organizationId matches the template's organizationId
- // If they don't match, throw a forbidden exception
- if (!template || organizationId !== template.organizationId) {
+ if (!template) {
this.exceptionsService.notFoundException(
DocumentTemplateExceptionMessages.TEMPLATE_001,
);
diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts
index 94dde098a..b80f3b21b 100644
--- a/backend/src/usecases/use-case.module.ts
+++ b/backend/src/usecases/use-case.module.ts
@@ -138,8 +138,10 @@ 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';
+import { CreateDocumentTemplateUsecase } from './documents/new_contracts/create-document-template.usecase';
+import { GetOneDocumentTemplateUseCase } from './documents/new_contracts/get-one-document-template.usecase';
+import { CreateDocumentContractUsecase } from './documents/new_contracts/create-document-contract.usecase';
+import { GetManyDocumentContractsUsecase } from './documents/new_contracts/get-many-document-contracts.usecase';
@Module({
imports: [
@@ -286,6 +288,7 @@ import { GetOneDocumentTemplateUseCase } from './documents/get-one-document-temp
DeleteTemplateUseCase,
GetAllTemplatesUsecase,
GetTemplatesForDownloadUsecase,
+ // NEW Templates
CreateDocumentTemplateUsecase,
GetOneDocumentTemplateUseCase,
// Contracts
@@ -302,6 +305,9 @@ import { GetOneDocumentTemplateUseCase } from './documents/get-one-document-temp
GetVolunteerContractHistoryUsecase,
GetVolunteerPendingContractsUsecase,
CancelContractUsecase,
+ // NEW Contracts
+ CreateDocumentContractUsecase,
+ GetManyDocumentContractsUsecase,
// Notifications
UpdateSettingsUsecase,
// Testing PDFs
@@ -434,6 +440,7 @@ import { GetOneDocumentTemplateUseCase } from './documents/get-one-document-temp
DeleteTemplateUseCase,
GetAllTemplatesUsecase,
GetTemplatesForDownloadUsecase,
+ // NEW Templates
CreateDocumentTemplateUsecase,
GetOneDocumentTemplateUseCase,
// Contracts
@@ -450,9 +457,12 @@ import { GetOneDocumentTemplateUseCase } from './documents/get-one-document-temp
GetVolunteerContractHistoryUsecase,
GetVolunteerPendingContractsUsecase,
CancelContractUsecase,
- GetVicStatisticsUsecase,
+ // NEW Contracts
+ CreateDocumentContractUsecase,
+ GetManyDocumentContractsUsecase,
// Notifications
UpdateSettingsUsecase,
+ GetVicStatisticsUsecase,
// Testing PDFs
GeneratePDFsUseCase,
],
From b47b0eb5b4cbbfce9c4048a13262ef7390446b0f Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Mon, 9 Sep 2024 14:36:50 +0300
Subject: [PATCH 17/55] feat: [Contracts] Update migration to include Contract
Status Enum
---
...81754175-DDLDocumentContractsSignature.ts} | 46 +++++++++++--------
1 file changed, 26 insertions(+), 20 deletions(-)
rename backend/src/migrations/{1725878984294-DDLDocumentContractsSignature.ts => 1725881754175-DDLDocumentContractsSignature.ts} (94%)
diff --git a/backend/src/migrations/1725878984294-DDLDocumentContractsSignature.ts b/backend/src/migrations/1725881754175-DDLDocumentContractsSignature.ts
similarity index 94%
rename from backend/src/migrations/1725878984294-DDLDocumentContractsSignature.ts
rename to backend/src/migrations/1725881754175-DDLDocumentContractsSignature.ts
index 3ff77e209..be52ede5e 100644
--- a/backend/src/migrations/1725878984294-DDLDocumentContractsSignature.ts
+++ b/backend/src/migrations/1725881754175-DDLDocumentContractsSignature.ts
@@ -1,9 +1,9 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
-export class DDLDocumentContractsSignature1725878984294
+export class DDLDocumentContractsSignature1725881754175
implements MigrationInterface
{
- name = 'DDLDocumentContractsSignature1725878984294';
+ name = 'DDLDocumentContractsSignature1725881754175';
public async up(queryRunner: QueryRunner): Promise {
await queryRunner.query(
@@ -12,6 +12,15 @@ export class DDLDocumentContractsSignature1725878984294
await queryRunner.query(
`CREATE INDEX "IDX_8c0295ec75967828329ef52ac3" ON "document_signature" ("created_on") `,
);
+ await queryRunner.query(
+ `CREATE TABLE "document_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_0e9c5bda0dd75f3bde7ae176c62" PRIMARY KEY ("id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_6dfe1cd0474df5d7d716bf59f0" ON "document_template" ("created_on") `,
+ );
+ await queryRunner.query(
+ `CREATE TYPE "public"."document_contract_status_enum" AS ENUM('CREATED', 'SCHEDULED', 'PENDING_VOLUNTEER_SIGNATURE', 'PENDING_APPROVAL_NGO', 'PENDING_NGO_REPRESENTATIVE_SIGNATURE', 'APPROVED', 'REJECTED_VOLUNTEER', 'REJECTED_NGO', 'ACTION_EXPIRED')`,
+ );
await queryRunner.query(
`CREATE TABLE "document_contract" ("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(), "status" "public"."document_contract_status_enum" NOT NULL DEFAULT 'CREATED', "document_number" text NOT NULL, "document_date" date NOT NULL, "document_start_date" date NOT NULL, "document_end_date" date NOT NULL, "file_path" text, "volunteer_data" jsonb NOT NULL, "volunteer_tutor_data" jsonb, "volunteer_id" uuid NOT NULL, "organization_id" uuid NOT NULL, "document_template_id" uuid, "created_by_admin_id" uuid NOT NULL, "ngo_legal_representative_signature_id" uuid, "volunteer_signature_id" uuid, "tutor_signature_id" uuid, CONSTRAINT "PK_bc0002326db7d928c061fc90953" PRIMARY KEY ("id"))`,
);
@@ -19,13 +28,13 @@ export class DDLDocumentContractsSignature1725878984294
`CREATE INDEX "IDX_0bc738c91555e8a0ef9836de02" ON "document_contract" ("created_on") `,
);
await queryRunner.query(
- `CREATE TABLE "document_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_0e9c5bda0dd75f3bde7ae176c62" PRIMARY KEY ("id"))`,
+ `ALTER TABLE "document_signature" ADD CONSTRAINT "FK_6e59057f55354293bf9f5e0a8df" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
- `CREATE INDEX "IDX_6dfe1cd0474df5d7d716bf59f0" ON "document_template" ("created_on") `,
+ `ALTER TABLE "document_template" ADD CONSTRAINT "FK_5b878af38db8ff501cbba07d97b" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
- `ALTER TABLE "document_signature" ADD CONSTRAINT "FK_6e59057f55354293bf9f5e0a8df" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ `ALTER TABLE "document_template" ADD CONSTRAINT "FK_efd8efceb4027c6e48af499e005" FOREIGN KEY ("created_by_admin_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "document_contract" ADD CONSTRAINT "FK_7617b71c917deb25a66df28843d" FOREIGN KEY ("volunteer_id") REFERENCES "volunteer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
@@ -48,12 +57,6 @@ export class DDLDocumentContractsSignature1725878984294
await queryRunner.query(
`ALTER TABLE "document_contract" ADD CONSTRAINT "FK_8586b2b6c023b4a93d301363004" FOREIGN KEY ("tutor_signature_id") REFERENCES "document_signature"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
- await queryRunner.query(
- `ALTER TABLE "document_template" ADD CONSTRAINT "FK_5b878af38db8ff501cbba07d97b" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
- );
- await queryRunner.query(
- `ALTER TABLE "document_template" ADD CONSTRAINT "FK_efd8efceb4027c6e48af499e005" FOREIGN KEY ("created_by_admin_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
- );
await queryRunner.query(`CREATE VIEW "DocumentContractListView" AS
SELECT
document_contract.id as document_id,
@@ -89,12 +92,6 @@ export class DDLDocumentContractsSignature1725878984294
['VIEW', 'DocumentContractListView', 'public'],
);
await queryRunner.query(`DROP VIEW "DocumentContractListView"`);
- await queryRunner.query(
- `ALTER TABLE "document_template" DROP CONSTRAINT "FK_efd8efceb4027c6e48af499e005"`,
- );
- await queryRunner.query(
- `ALTER TABLE "document_template" DROP CONSTRAINT "FK_5b878af38db8ff501cbba07d97b"`,
- );
await queryRunner.query(
`ALTER TABLE "document_contract" DROP CONSTRAINT "FK_8586b2b6c023b4a93d301363004"`,
);
@@ -117,16 +114,25 @@ export class DDLDocumentContractsSignature1725878984294
`ALTER TABLE "document_contract" DROP CONSTRAINT "FK_7617b71c917deb25a66df28843d"`,
);
await queryRunner.query(
- `ALTER TABLE "document_signature" DROP CONSTRAINT "FK_6e59057f55354293bf9f5e0a8df"`,
+ `ALTER TABLE "document_template" DROP CONSTRAINT "FK_efd8efceb4027c6e48af499e005"`,
);
await queryRunner.query(
- `DROP INDEX "public"."IDX_6dfe1cd0474df5d7d716bf59f0"`,
+ `ALTER TABLE "document_template" DROP CONSTRAINT "FK_5b878af38db8ff501cbba07d97b"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_signature" DROP CONSTRAINT "FK_6e59057f55354293bf9f5e0a8df"`,
);
- await queryRunner.query(`DROP TABLE "document_template"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_0bc738c91555e8a0ef9836de02"`,
);
await queryRunner.query(`DROP TABLE "document_contract"`);
+ await queryRunner.query(
+ `DROP TYPE "public"."document_contract_status_enum"`,
+ );
+ await queryRunner.query(
+ `DROP INDEX "public"."IDX_6dfe1cd0474df5d7d716bf59f0"`,
+ );
+ await queryRunner.query(`DROP TABLE "document_template"`);
await queryRunner.query(
`DROP INDEX "public"."IDX_8c0295ec75967828329ef52ac3"`,
);
From 2c415cc9d0814db86384f5b6a5e140781ad53543 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Mon, 9 Sep 2024 17:18:54 +0300
Subject: [PATCH 18/55] feat: [Contracts] Change user birthday, document issue
date and expiration from timestamp to date maintaining RO Timezone
---
...5889826591-ChangeTimestampToDateForUser.ts | 38 +++++++++++++++++++
.../entities/user-personal-data.entity.ts | 4 +-
.../src/modules/user/entities/user.entity.ts | 2 +-
3 files changed, 41 insertions(+), 3 deletions(-)
create mode 100644 backend/src/migrations/1725889826591-ChangeTimestampToDateForUser.ts
diff --git a/backend/src/migrations/1725889826591-ChangeTimestampToDateForUser.ts b/backend/src/migrations/1725889826591-ChangeTimestampToDateForUser.ts
new file mode 100644
index 000000000..2b20cb1d4
--- /dev/null
+++ b/backend/src/migrations/1725889826591-ChangeTimestampToDateForUser.ts
@@ -0,0 +1,38 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class ChangeTimestampToDateForUser1725889826591
+ implements MigrationInterface
+{
+ name = 'ChangeTimestampToDateForUser1725889826591';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" ALTER COLUMN "identity_document_issue_date" TYPE date USING ("identity_document_issue_date"::timestamp AT TIME ZONE 'GMT+3')::date`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" ALTER COLUMN "identity_document_expiration_date" TYPE date USING ("identity_document_expiration_date"::timestamp AT TIME ZONE 'GMT+3')::date`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user" ALTER COLUMN "birthday" TYPE date USING ("birthday"::timestamp AT TIME ZONE 'GMT+3')::date`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "birthday"`);
+ await queryRunner.query(
+ `ALTER TABLE "user" ADD "birthday" TIMESTAMP WITH TIME ZONE`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" DROP COLUMN "identity_document_expiration_date"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" ADD "identity_document_expiration_date" TIMESTAMP WITH TIME ZONE NOT NULL`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" DROP COLUMN "identity_document_issue_date"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" ADD "identity_document_issue_date" TIMESTAMP WITH TIME ZONE NOT NULL`,
+ );
+ }
+}
diff --git a/backend/src/modules/user/entities/user-personal-data.entity.ts b/backend/src/modules/user/entities/user-personal-data.entity.ts
index 3c310ed48..c4a4491ea 100644
--- a/backend/src/modules/user/entities/user-personal-data.entity.ts
+++ b/backend/src/modules/user/entities/user-personal-data.entity.ts
@@ -14,11 +14,11 @@ export class UserPersonalDataEntity extends BaseEntity {
@Column({ type: 'text', name: 'address' })
address: string;
- @Column({ type: 'timestamptz', name: 'identity_document_issue_date' })
+ @Column({ type: 'date', name: 'identity_document_issue_date' })
identityDocumentIssueDate: Date;
@Column({
- type: 'timestamptz',
+ type: 'date',
name: 'identity_document_expiration_date',
})
identityDocumentExpirationDate: Date;
diff --git a/backend/src/modules/user/entities/user.entity.ts b/backend/src/modules/user/entities/user.entity.ts
index d5837c624..5450ad6f7 100644
--- a/backend/src/modules/user/entities/user.entity.ts
+++ b/backend/src/modules/user/entities/user.entity.ts
@@ -68,7 +68,7 @@ export class RegularUserEntity extends UserEntity {
@Column({ type: 'text', name: 'last_name' })
lastName: string;
- @Column({ type: 'timestamptz', name: 'birthday', nullable: true })
+ @Column({ type: 'date', name: 'birthday', nullable: true })
birthday: Date;
@Column({ type: 'varchar', name: 'sex', enum: SEX, nullable: true })
From afdf0299da08e4b498d246de1044e9e921b8e9f1 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Mon, 9 Sep 2024 17:24:03 +0300
Subject: [PATCH 19/55] feat: [Contracts] Remove unused migration
---
.../1725454854964-AddDocumentTemplate.ts | 33 -------------------
1 file changed, 33 deletions(-)
delete mode 100644 backend/src/migrations/1725454854964-AddDocumentTemplate.ts
diff --git a/backend/src/migrations/1725454854964-AddDocumentTemplate.ts b/backend/src/migrations/1725454854964-AddDocumentTemplate.ts
deleted file mode 100644
index c75491a62..000000000
--- a/backend/src/migrations/1725454854964-AddDocumentTemplate.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-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"`);
- }
-}
From 02d66ae9de17e52a741b0cc0f31a9156d66b0565 Mon Sep 17 00:00:00 2001
From: luciatugui
Date: Mon, 9 Sep 2024 17:33:37 +0300
Subject: [PATCH 20/55] wip: [Contracts] - guradian fields in Identity Data for
volunteers under 16
---
mobile/src/assets/locales/en/translation.json | 50 +++-
mobile/src/assets/locales/ro/translation.json | 50 +++-
.../interfaces/user-profile.interface.ts | 11 +
mobile/src/screens/IdentityData.tsx | 261 ++++++++++++++++--
4 files changed, 340 insertions(+), 32 deletions(-)
diff --git a/mobile/src/assets/locales/en/translation.json b/mobile/src/assets/locales/en/translation.json
index e20a6545b..3f458b708 100644
--- a/mobile/src/assets/locales/en/translation.json
+++ b/mobile/src/assets/locales/en/translation.json
@@ -234,6 +234,14 @@
"description": "Enter your ID card details. These will be used to generate the volunteer contract.",
"privacy_policy": "Privacy Policy",
"form": {
+ "cnp": {
+ "label": "CNP",
+ "placeholder": "Enter CNP",
+ "matches": "CNP must contain only digits",
+ "required": "CNP is mandatory",
+ "length": "CNP must contain {{number}} digits",
+ "birthday_mismatch": "The birth date extracted from CNP does not match the one in the account data"
+ },
"series": {
"label": "Identity document (ID) series",
"matches": "The series must contain only letters",
@@ -260,14 +268,54 @@
"label": "Date of issue",
"required": "Date of issue is mandatory"
},
+ "issued_by": {
+ "label": "Issued by",
+ "placeholder": "Entity that issued the document (SPCLEP...)",
+ "required": "The name of the entity that issued the document is mandatory"
+ },
"expiration_date": {
"label": "Expiry date",
"required": "Expiry date is mandatory"
},
+ "guardian": {
+ "name": {
+ "label": "Full name",
+ "placeholder": "John Doe",
+ "required": "Legal guardian's full name is mandatory"
+ },
+ "series": {
+ "label": "Identity document (ID) series",
+ "matches": "The series must contain only letters",
+ "placeholder": "Enter series (GZ, RX...)",
+ "length": "The series must have {{number}} characters",
+ "required": "The series is mandatory"
+ },
+ "number": {
+ "label": "Identity document number (CI)",
+ "placeholder": "Enter the 6-digit number",
+ "matches": "The number must contain only digits",
+ "length": "The number must have {{number}} digits",
+ "required": "The number is mandatory"
+ },
+ "email": {
+ "label": "Email",
+ "placeholder": "Enter email address",
+ "required": "Email address is mandatory",
+ "pattern": "The email format is invalid"
+ },
+ "phone": {
+ "label": "Phone",
+ "placeholder": "Enter phone number",
+ "required": "The phone number is mandatory",
+ "matches": "The phone number must contain only digits",
+ "length": "The phone number must have {{number}} characters"
+ }
+ },
"submit": {
"success": "The dates have been changed!"
}
- }
+ },
+ "legal_gardian_data_required": "Attention! For volunteers under the age of 16, the legal guardian data is required. This will be included in the volunteer contract."
},
"change_password": {
"title": "Change password",
diff --git a/mobile/src/assets/locales/ro/translation.json b/mobile/src/assets/locales/ro/translation.json
index 1cd46b65e..020d39773 100644
--- a/mobile/src/assets/locales/ro/translation.json
+++ b/mobile/src/assets/locales/ro/translation.json
@@ -234,6 +234,14 @@
"description": "Introdu datele din actul de identitate. Acestea vor fi folosite pentru generarea contractului de voluntariat.",
"privacy_policy": "Politica de Confidențialitate",
"form": {
+ "cnp": {
+ "label": "CNP",
+ "placeholder": "Introdu CNP-ul",
+ "matches": "CNP-ul trebuie să conțină doar cifre",
+ "required": "CNP-ul este obligatoriu",
+ "length": "CNP-ul trebuie să conțină {{number}} cifre",
+ "birthday_mismatch": "Data de naștere extrasă din CNP nu corespunde cu cea din datele contului de utilizator"
+ },
"series": {
"label": "Serie document identitate (CI)",
"matches": "Seria trebuie să conțină doar litere",
@@ -260,14 +268,54 @@
"label": "Data emitere",
"required": "Data emiterii este obligatorie"
},
+ "issued_by": {
+ "label": "Eliberat de",
+ "placeholder": "Entitatea care a emis documentul (SPCLEP...)",
+ "required": "Denumirea entității care a emis documentul este obligatorie"
+ },
"expiration_date": {
"label": "Data expirare",
"required": "Data expirării este obligatorie"
},
+ "guardian": {
+ "name": {
+ "label": "Nume și prenume",
+ "placeholder": "Ion Popescu",
+ "required": "Numele și prenumele tutorelui legal sunt obligatorii"
+ },
+ "series": {
+ "label": "Serie document identitate (CI)",
+ "matches": "Seria trebuie să conțină doar litere",
+ "placeholder": "Introdu seria (GZ, RX...)",
+ "length": "Seria trebuie să aibă {{number}} caractere",
+ "required": "Seria este obligatorie"
+ },
+ "number": {
+ "label": "Număr document identitate (CI)",
+ "placeholder": "Introdu numărul din 6 cifre",
+ "matches": "Numărul trebuie să conțină doar cifre",
+ "length": "Numărul trebuie să aibă {{number}} caractere",
+ "required": "Numărul este obligatoriu"
+ },
+ "email": {
+ "label": "Email",
+ "placeholder": "Introdu adresa de email",
+ "required": "Adresa de email este obligatorie",
+ "pattern": "Formatul email-ului nu este valid"
+ },
+ "phone": {
+ "label": "Telefon",
+ "placeholder": "Introdu numărul de telefon",
+ "required": "Numărul de telefon este obligatoriu",
+ "matches": "Numărul de telefon trebuie să conțină doar cifre",
+ "length": "Numărul de telefon trebuie să aibă {{number}} caractere"
+ }
+ },
"submit": {
"success": "Datele au fost modificate!"
}
- }
+ },
+ "legal_gardian_data_required": "Atenție! Pentru voluntarii sub 16 ani, sunt necesare datele tutorelui legal. Acestea vor fi incluse în contractul de voluntariat."
},
"change_password": {
"title": "Schimbă parola",
diff --git a/mobile/src/common/interfaces/user-profile.interface.ts b/mobile/src/common/interfaces/user-profile.interface.ts
index 0534713e5..adab63285 100644
--- a/mobile/src/common/interfaces/user-profile.interface.ts
+++ b/mobile/src/common/interfaces/user-profile.interface.ts
@@ -5,11 +5,21 @@ import { IOrganizationVolunteer } from './organization-list-item.interface';
export interface IUserPersonalData {
id: string;
+ identityDocumentCNP: string;
identityDocumentSeries: string;
identityDocumentNumber: string;
address: string;
identityDocumentIssueDate: Date;
identityDocumentExpirationDate: Date;
+ identityDocumentIssuedBy: string;
+}
+
+export interface ILegalGuardianData {
+ name: string;
+ identityDocumentSeries: string;
+ identityDocumentNumber: string;
+ email: string;
+ phone: string;
}
export interface INotificationsSettings {
@@ -27,6 +37,7 @@ export interface IUserProfile {
sex: Sex;
location?: ICity;
userPersonalData: IUserPersonalData;
+ legalGuardianData?: ILegalGuardianData;
activeOrganization: IOrganizationVolunteer | null;
myOrganizations: IOrganizationVolunteer[];
profilePicture?: string;
diff --git a/mobile/src/screens/IdentityData.tsx b/mobile/src/screens/IdentityData.tsx
index fe8e4fc9a..5e7ea4396 100644
--- a/mobile/src/screens/IdentityData.tsx
+++ b/mobile/src/screens/IdentityData.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import PageLayout from '../layouts/PageLayout';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
@@ -18,58 +18,183 @@ import Paragraph from '../components/Paragraph';
import { REGEX } from '../common/constants/constants';
import { useUserProfile } from '../store/profile/profile.selector';
import { usePaddingTop } from '../hooks/usePaddingTop';
+import { differenceInYears, parseISO } from 'date-fns';
export type IdentityDataFormTypes = {
+ identityDocumentCNP: string;
identityDocumentSeries: string;
identityDocumentNumber: string;
address: string;
identityDocumentIssueDate: Date;
+ identityDocumentIssuedBy: string;
identityDocumentExpirationDate: Date;
+ guardianName?: string;
+ guardianIdentityDocumentSeries?: string;
+ guardianIdentityDocumentNumber?: string;
+ guardianEmail?: string;
+ guardianPhone?: string;
};
-const schema = yup.object({
- identityDocumentSeries: yup
- .string()
- .matches(REGEX.STRINGS_ONLY, `${i18n.t('identity_data:form.series.matches')}`)
- .required(`${i18n.t('identity_data:form.series.required')}`)
- .length(2, `${i18n.t('identity_data:form.series.length', { number: 2 })}`),
- identityDocumentNumber: yup
- .string()
- .matches(REGEX.NUMBERS_ONLY, `${i18n.t('identity_data:form.number.matches')}`)
- .required(`${i18n.t('identity_data:form.number.required')}`)
- .length(6, `${i18n.t('identity_data:form.number.length', { number: 6 })}`),
- address: yup
- .string()
- .required(`${i18n.t('identity_data:form.address.required')}`)
- .min(2, `${i18n.t('identity_data:form.address.min', { value: 2 })}`)
- .max(100, `${i18n.t('identity_data:form.address.max', { value: 100 })}`),
- identityDocumentIssueDate: yup
- .date()
- .required(`${i18n.t('identity_data:form.issue_date.required')}`),
- identityDocumentExpirationDate: yup
- .date()
- .required(`${i18n.t('identity_data:form.expiration_date.required')}`),
-});
+const schema = (isUserOver16: boolean, userBirthday: Date | undefined) =>
+ yup.object({
+ identityDocumentCNP: yup
+ .string()
+ .matches(REGEX.NUMBERS_ONLY, `${i18n.t('identity_data:form.cnp.matches')}`)
+ .required(`${i18n.t('identity_data:form.cnp.required')}`)
+ .length(13, `${i18n.t('identity_data:form.cnp.length', { number: 13 })}`)
+ // check if the birthday extracted from the CNP matches the user birthday
+ .test(
+ 'cnp-birthday-match',
+ `${i18n.t('identity_data:form.cnp.birthday_mismatch')}`,
+ (value) => {
+ if (!userBirthday || !value) {
+ return true;
+ }
+ const cnpBirthday = getBirthdayFromCNP(value);
+ // TODO: remove this after testing
+ console.log('cnpBirthday: 🎂', cnpBirthday);
+ console.log('userBirthday: 🎂', userBirthday);
+ return cnpBirthday ? cnpBirthday.getTime() === new Date(userBirthday).getTime() : true;
+ },
+ ),
+ identityDocumentSeries: yup
+ .string()
+ .matches(REGEX.STRINGS_ONLY, `${i18n.t('identity_data:form.series.matches')}`)
+ .required(`${i18n.t('identity_data:form.series.required')}`)
+ .length(2, `${i18n.t('identity_data:form.series.length', { number: 2 })}`),
+ identityDocumentNumber: yup
+ .string()
+ .matches(REGEX.NUMBERS_ONLY, `${i18n.t('identity_data:form.number.matches')}`)
+ .required(`${i18n.t('identity_data:form.number.required')}`)
+ .length(6, `${i18n.t('identity_data:form.number.length', { number: 6 })}`),
+ address: yup
+ .string()
+ .required(`${i18n.t('identity_data:form.address.required')}`)
+ .min(2, `${i18n.t('identity_data:form.address.min', { value: 2 })}`)
+ .max(100, `${i18n.t('identity_data:form.address.max', { value: 100 })}`),
+ identityDocumentIssueDate: yup
+ .date()
+ .required(`${i18n.t('identity_data:form.issue_date.required')}`),
+ identityDocumentIssuedBy: yup
+ .string()
+ .required(`${i18n.t('identity_data:form.issued_by.required')}`),
+ identityDocumentExpirationDate: yup
+ .date()
+ .required(`${i18n.t('identity_data:form.expiration_date.required')}`),
+ guardianName: yup.string().when([], {
+ is: () => !isUserOver16,
+ then: (schema) => schema.required(`${i18n.t('identity_data:form.guardian.name.required')}`),
+ otherwise: (schema) => schema.optional(),
+ }),
+ guardianIdentityDocumentSeries: yup
+ .string()
+ .matches(REGEX.STRINGS_ONLY, `${i18n.t('identity_data:form.guardian.series.matches')}`)
+ .length(2, `${i18n.t('identity_data:form.guardian.series.length', { number: 2 })}`)
+ .when([], {
+ is: () => !isUserOver16,
+ then: (schema) =>
+ schema.required(`${i18n.t('identity_data:form.guardian.series.required')}`),
+ otherwise: (schema) => schema.optional(),
+ }),
+ guardianIdentityDocumentNumber: yup
+ .string()
+ .matches(REGEX.NUMBERS_ONLY, `${i18n.t('identity_data:form.guardian.number.matches')}`)
+ .length(6, `${i18n.t('identity_data:form.guardian.number.length', { number: 6 })}`)
+ .when([], {
+ is: () => !isUserOver16,
+ then: (schema) =>
+ schema.required(`${i18n.t('identity_data:form.guardian.number.required')}`),
+ otherwise: (schema) => schema.optional(),
+ }),
+ guardianEmail: yup
+ .string()
+ .email(`${i18n.t('identity_data:form.guardian.email.pattern')}`)
+ .when([], {
+ is: () => !isUserOver16,
+ then: (schema) =>
+ schema.required(`${i18n.t('identity_data:form.guardian.email.required')}`),
+ otherwise: (schema) => schema.optional(),
+ }),
+ guardianPhone: yup
+ .string()
+ .matches(REGEX.NUMBERS_ONLY, `${i18n.t('identity_data:form.guardian.phone.matches')}`)
+ .length(9, `${i18n.t('identity_data:form.guardian.phone.length', { number: 10 })}`)
+ .when([], {
+ is: () => !isUserOver16,
+ then: (schema) =>
+ schema.required(`${i18n.t('identity_data:form.guardian.phone.required')}`),
+ otherwise: (schema) => schema.optional(),
+ }),
+ });
+
+const isOver16 = (birthday: string | Date) => {
+ const birthdayDate = typeof birthday === 'string' ? parseISO(birthday) : birthday;
+ const today = new Date();
+ const age = differenceInYears(today, birthdayDate);
+ return age >= 16;
+};
+
+const isOver16FromCNP = (cnp: string) => {
+ // we don't need to perform the calculation before the user has entered all the necessary digits to calculate
+ if (cnp.length < 7) {
+ return true;
+ }
+
+ // CNP example: 2980825... -> 1998-08-25
+ // 6000825... -> 2000-08-25
+
+ // if first digit is above 5, then the birth year is 2000+
+ const yearPrefix = parseInt(cnp[0], 10) < 5 ? '19' : '20';
+ const year = (yearPrefix + cnp.substring(1, 3)).toString();
+ const month = cnp.substring(3, 5);
+ const day = cnp.substring(5, 7);
+ const birthday = new Date(`${year}-${month}-${day}`);
+
+ const age = differenceInYears(new Date(), birthday);
+ return age >= 16;
+};
+
+const getBirthdayFromCNP = (cnp: string): Date | null => {
+ if (cnp.length < 7) {
+ return null;
+ }
+
+ const yearPrefix = parseInt(cnp[0], 10) < 5 ? '19' : '20';
+ const year = (yearPrefix + cnp.substring(1, 3)).toString();
+ const month = cnp.substring(3, 5);
+ const day = cnp.substring(5, 7);
+ return new Date(`${year}-${month}-${day}`);
+};
const IdentityData = ({ navigation, route }: any) => {
- const { userProfile } = useUserProfile();
- const { t } = useTranslation('identity_data');
const paddingTop = usePaddingTop();
+ const { t } = useTranslation('identity_data');
+
+ const { userProfile } = useUserProfile();
+ // try to decide if the user is over 16 based on the birthday or the CNP
+ const [isUserOver16, setIsUserOver16] = useState(
+ userProfile?.birthday
+ ? isOver16(userProfile?.birthday)
+ : userProfile?.userPersonalData.identityDocumentCNP
+ ? isOver16FromCNP(userProfile?.userPersonalData.identityDocumentCNP)
+ : true,
+ );
+
+ const { isLoading: isUpdateingPersonalData, mutate: updateUserPersonalData } =
+ useUpdateUserPersonalDataMutation();
const {
control,
handleSubmit,
formState: { errors },
+ watch,
reset,
} = useForm({
mode: 'onSubmit',
reValidateMode: 'onChange',
- resolver: yupResolver(schema),
+ resolver: yupResolver(schema(isUserOver16, userProfile?.birthday)),
});
- const { isLoading: isUpdateingPersonalData, mutate: updateUserPersonalData } =
- useUpdateUserPersonalDataMutation();
-
useEffect(() => {
const { userPersonalData } = userProfile as IUserProfile;
@@ -81,12 +206,26 @@ const IdentityData = ({ navigation, route }: any) => {
// init form data with the user profile personal data
reset({
...formData,
+ // TODO: add the new fields
identityDocumentIssueDate: new Date(formData.identityDocumentIssueDate),
identityDocumentExpirationDate: new Date(formData.identityDocumentExpirationDate),
});
}
}, [userProfile, reset]);
+ useEffect(() => {
+ // if the user has a birthday, then we don't need to check the CNP
+ if (userProfile?.birthday) {
+ return;
+ }
+ const subscription = watch((value, { name }) => {
+ if (name === 'identityDocumentCNP') {
+ setIsUserOver16(isOver16FromCNP(value.identityDocumentCNP || ''));
+ }
+ });
+ return () => subscription.unsubscribe();
+ }, [watch, userProfile?.birthday]);
+
const onPrivacyPolicyPress = () => {
Linking.openURL(`${process.env.EXPO_PUBLIC_PRIVACY_POLICY_LINK}`);
};
@@ -95,6 +234,7 @@ const IdentityData = ({ navigation, route }: any) => {
updateUserPersonalData(
{
...payload,
+ // TODO: add the new fields
identityDocumentSeries: payload.identityDocumentSeries.toLocaleUpperCase(),
},
{
@@ -129,6 +269,14 @@ const IdentityData = ({ navigation, route }: any) => {
{`${t('description')}`}
+
{
max={new Date(2200, 0, 0)}
disabled={isUpdateingPersonalData}
/>
+
+ {!isUserOver16 && (
+ <>
+ {`${t('legal_gardian_data_required')}`}
+
+
+
+
+
+ >
+ )}
);
From 2a3287b92c50011e019ef5b9036fd3aeae74ebdf Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Tue, 10 Sep 2024 10:27:25 +0300
Subject: [PATCH 21/55] feat: [Contracts] Fix DateRangePicker typescript errors
to pass the FE build
---
frontend/src/components/AccessRequestTable.tsx | 15 +++++++++------
.../src/components/ActionsArchiveTable.tsx | 9 ++++++---
frontend/src/components/ActivityLogTable.tsx | 18 ++++++++++++------
frontend/src/pages/GenerateContract.tsx | 2 +-
4 files changed, 28 insertions(+), 16 deletions(-)
diff --git a/frontend/src/components/AccessRequestTable.tsx b/frontend/src/components/AccessRequestTable.tsx
index 729f1e880..2b1c30a5a 100644
--- a/frontend/src/components/AccessRequestTable.tsx
+++ b/frontend/src/components/AccessRequestTable.tsx
@@ -352,21 +352,24 @@ const AccessRequestTable = ({
);
};
- const onCreatedOnRangeChange = ([createdOnStart, createdOnEnd]: Date[]) => {
+ const onCreatedOnRangeChange = ([createdOnStart, createdOnEnd]: [Date | null, Date | null]) => {
setQuery(
{
- createdOnStart,
- createdOnEnd,
+ createdOnStart: createdOnStart ?? undefined,
+ createdOnEnd: createdOnEnd ?? undefined,
},
'replaceIn',
);
};
- const onRejectedOnRangeChange = ([rejectedOnStart, rejectedOnEnd]: Date[]) => {
+ const onRejectedOnRangeChange = ([rejectedOnStart, rejectedOnEnd]: [
+ Date | null,
+ Date | null,
+ ]) => {
setQuery(
{
- rejectedOnStart,
- rejectedOnEnd,
+ rejectedOnStart: rejectedOnStart ?? undefined,
+ rejectedOnEnd: rejectedOnEnd ?? undefined,
},
'replaceIn',
);
diff --git a/frontend/src/components/ActionsArchiveTable.tsx b/frontend/src/components/ActionsArchiveTable.tsx
index 424da50ca..b80a83d3b 100644
--- a/frontend/src/components/ActionsArchiveTable.tsx
+++ b/frontend/src/components/ActionsArchiveTable.tsx
@@ -117,10 +117,13 @@ const ActionsArchiveTable = ({ query, setQuery, volunteerId }: ActionsArchiveTab
});
};
- const onActionDateRangeChange = ([actionStartDate, actionEndDate]: Date[]) => {
+ const onActionDateRangeChange = ([actionStartDate, actionEndDate]: [
+ Date | null,
+ Date | null,
+ ]) => {
setQuery({
- actionStartDate,
- actionEndDate,
+ actionStartDate: actionStartDate ?? undefined,
+ actionEndDate: actionEndDate ?? undefined,
});
};
diff --git a/frontend/src/components/ActivityLogTable.tsx b/frontend/src/components/ActivityLogTable.tsx
index d066eadaa..a93ab1f82 100644
--- a/frontend/src/components/ActivityLogTable.tsx
+++ b/frontend/src/components/ActivityLogTable.tsx
@@ -375,17 +375,23 @@ const ActivityLogTable = ({
setQuery({ status: status?.key });
};
- const onExecutionOnRangeChange = ([executionDateStart, executionDateEnd]: Date[]) => {
+ const onExecutionOnRangeChange = ([executionDateStart, executionDateEnd]: [
+ Date | null,
+ Date | null,
+ ]) => {
setQuery({
- executionDateStart,
- executionDateEnd,
+ executionDateStart: executionDateStart ?? undefined,
+ executionDateEnd: executionDateEnd ?? undefined,
});
};
- const onRegistrationOnRangeChange = ([registrationDateStart, registrationDateEnd]: Date[]) => {
+ const onRegistrationOnRangeChange = ([registrationDateStart, registrationDateEnd]: [
+ Date | null,
+ Date | null,
+ ]) => {
setQuery({
- registrationDateStart,
- registrationDateEnd,
+ registrationDateStart: registrationDateStart ?? undefined,
+ registrationDateEnd: registrationDateEnd ?? undefined,
});
};
diff --git a/frontend/src/pages/GenerateContract.tsx b/frontend/src/pages/GenerateContract.tsx
index 16ec54aa9..e9261fc87 100644
--- a/frontend/src/pages/GenerateContract.tsx
+++ b/frontend/src/pages/GenerateContract.tsx
@@ -147,7 +147,7 @@ export const GenerateContract = () => {
setCurrentStep(currentStep - 1);
}
};
- const onSelectTemplate = (template: ContractTemplate) => {
+ const onSelectTemplate = (template: ContractTemplate[]) => {
console.log(template);
};
From 0238e09036cba6ff5db5ecd7f17aceb639896935 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Tue, 10 Sep 2024 10:58:39 +0300
Subject: [PATCH 22/55] feat: [Contracts] Add CNP, issuedBy and legalGuardian
data to the user personal data
---
.../user/dto/update-user-personal-data.dto.ts | 43 +++++++++++++++++++
...25953984933-AddUserPersonalDataAndTutor.ts | 35 +++++++++++++++
.../entities/user-personal-data.entity.ts | 12 +++++-
.../user/models/user-personal-data.model.ts | 19 +++++++-
.../user/update-user-personal-data.usecase.ts | 5 +--
5 files changed, 109 insertions(+), 5 deletions(-)
create mode 100644 backend/src/migrations/1725953984933-AddUserPersonalDataAndTutor.ts
diff --git a/backend/src/api/_mobile/user/dto/update-user-personal-data.dto.ts b/backend/src/api/_mobile/user/dto/update-user-personal-data.dto.ts
index 2c9b09b02..ddb05098e 100644
--- a/backend/src/api/_mobile/user/dto/update-user-personal-data.dto.ts
+++ b/backend/src/api/_mobile/user/dto/update-user-personal-data.dto.ts
@@ -1,13 +1,45 @@
+import { Type } from 'class-transformer';
import {
IsDate,
IsNotEmpty,
+ IsObject,
+ IsOptional,
IsString,
Length,
MaxLength,
MinLength,
+ ValidateNested,
} from 'class-validator';
+class LegalGuardianDto {
+ @IsString()
+ @IsNotEmpty()
+ @MinLength(2)
+ @MaxLength(100)
+ name: string;
+
+ @IsString()
+ @IsNotEmpty()
+ @Length(2)
+ identityDocumentSeries: string;
+
+ @IsString()
+ @IsNotEmpty()
+ @Length(6)
+ identityDocumentNumber: string;
+
+ @IsString()
+ email: string;
+
+ @IsString()
+ phone: string;
+}
export class UpdateUserPersonalDataDto {
+ @IsString()
+ @IsNotEmpty()
+ @Length(13)
+ cnp: string;
+
@IsString()
@IsNotEmpty()
@Length(2)
@@ -29,4 +61,15 @@ export class UpdateUserPersonalDataDto {
@IsDate()
identityDocumentExpirationDate: Date;
+
+ @IsString()
+ @IsNotEmpty()
+ @MaxLength(100)
+ identityDocumentIssuedBy: string;
+
+ @IsObject()
+ @IsOptional()
+ @ValidateNested()
+ @Type(() => LegalGuardianDto)
+ legalGuardian?: LegalGuardianDto;
}
diff --git a/backend/src/migrations/1725953984933-AddUserPersonalDataAndTutor.ts b/backend/src/migrations/1725953984933-AddUserPersonalDataAndTutor.ts
new file mode 100644
index 000000000..1ef63c5c5
--- /dev/null
+++ b/backend/src/migrations/1725953984933-AddUserPersonalDataAndTutor.ts
@@ -0,0 +1,35 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddUserPersonalDataAndTutor1725953984933
+ implements MigrationInterface
+{
+ name = 'AddUserPersonalDataAndTutor1725953984933';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "user_personal_data" ADD "cnp" text`);
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" ADD "identity_document_issued_by" text`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" ADD "legal_guardian" jsonb`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" DROP CONSTRAINT "UQ_a43393c324223214daef1914850"`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" ADD CONSTRAINT "UQ_a43393c324223214daef1914850" UNIQUE ("identity_document_number")`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" DROP COLUMN "legal_guardian"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" DROP COLUMN "issued_by"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user_personal_data" DROP COLUMN "cnp"`,
+ );
+ }
+}
diff --git a/backend/src/modules/user/entities/user-personal-data.entity.ts b/backend/src/modules/user/entities/user-personal-data.entity.ts
index c4a4491ea..2acf2d2aa 100644
--- a/backend/src/modules/user/entities/user-personal-data.entity.ts
+++ b/backend/src/modules/user/entities/user-personal-data.entity.ts
@@ -1,14 +1,18 @@
import { BaseEntity } from 'src/infrastructure/base/base-entity';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
+import { ILegalGuardian } from '../models/user-personal-data.model';
@Entity({ name: 'user_personal_data' })
export class UserPersonalDataEntity extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
+ @Column({ type: 'text', name: 'cnp', nullable: true })
+ cnp: string;
+
@Column({ type: 'text', name: 'identity_document_series' })
identityDocumentSeries: string;
- @Column({ type: 'text', name: 'identity_document_number', unique: true })
+ @Column({ type: 'text', name: 'identity_document_number' })
identityDocumentNumber: string;
@Column({ type: 'text', name: 'address' })
@@ -22,4 +26,10 @@ export class UserPersonalDataEntity extends BaseEntity {
name: 'identity_document_expiration_date',
})
identityDocumentExpirationDate: Date;
+
+ @Column({ type: 'text', name: 'identity_document_issued_by', nullable: true })
+ identityDocumentIssuedBy: string;
+
+ @Column({ type: 'jsonb', name: 'legal_guardian', nullable: true })
+ legalGuardian: ILegalGuardian;
}
diff --git a/backend/src/modules/user/models/user-personal-data.model.ts b/backend/src/modules/user/models/user-personal-data.model.ts
index 0cd7b55b5..3e9393783 100644
--- a/backend/src/modules/user/models/user-personal-data.model.ts
+++ b/backend/src/modules/user/models/user-personal-data.model.ts
@@ -2,11 +2,22 @@ import { UserPersonalDataEntity } from '../entities/user-personal-data.entity';
export interface IUserPersonalDataModel {
id: string;
+ cnp: string;
+ address: string;
identityDocumentSeries: string;
identityDocumentNumber: string;
- address: string;
identityDocumentIssueDate: Date;
identityDocumentExpirationDate: Date;
+ identityDocumentIssuedBy: string;
+ legalGuardian?: ILegalGuardian;
+}
+
+export interface ILegalGuardian {
+ name: string;
+ identityDocumentSeries: string;
+ identityDocumentNumber: string;
+ email: string;
+ phone: string;
}
export type CreateUserPersonalDataOptions = Omit;
@@ -20,10 +31,13 @@ export class UserPersonalDataTransformer {
if (!entity) return null;
return {
id: entity.id,
+ cnp: entity.cnp,
identityDocumentSeries: entity.identityDocumentSeries,
identityDocumentNumber: entity.identityDocumentNumber,
identityDocumentIssueDate: entity.identityDocumentIssueDate,
identityDocumentExpirationDate: entity.identityDocumentExpirationDate,
+ identityDocumentIssuedBy: entity.identityDocumentIssuedBy,
+ legalGuardian: entity.legalGuardian,
address: entity.address,
};
}
@@ -38,6 +52,9 @@ export class UserPersonalDataTransformer {
entity.identityDocumentIssueDate = model.identityDocumentIssueDate;
entity.identityDocumentNumber = model.identityDocumentNumber;
entity.identityDocumentSeries = model.identityDocumentSeries;
+ entity.identityDocumentIssuedBy = model.identityDocumentIssuedBy;
+ entity.legalGuardian = model.legalGuardian;
+ entity.cnp = model.cnp;
return entity;
}
}
diff --git a/backend/src/usecases/user/update-user-personal-data.usecase.ts b/backend/src/usecases/user/update-user-personal-data.usecase.ts
index d1c528a58..944e0f835 100644
--- a/backend/src/usecases/user/update-user-personal-data.usecase.ts
+++ b/backend/src/usecases/user/update-user-personal-data.usecase.ts
@@ -45,9 +45,8 @@ export class UpdateUserPersonalDataUsecase
// 4. check if the user has personal data
if (!user.userPersonalData) {
// 4.1 if not create new personal data entity for the user
- userIdentityData = await this.userService.createUserPersonalData(
- personalData,
- );
+ userIdentityData =
+ await this.userService.createUserPersonalData(personalData);
// 4.2 save the data to the user
await this.userService.updateRegularUser(id, {
From 1435e70b3dbc4fca6ee37e1a2198aeb5cbdc6b85 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Tue, 10 Sep 2024 13:59:17 +0300
Subject: [PATCH 23/55] feat: [Contracts] Integrate the new User Personal Data
fields in mobile's model
---
.../user-personal-data.presenter.ts | 23 ++++-
.../interfaces/user-profile.interface.ts | 4 +-
mobile/src/components/LatestNews.tsx | 2 +-
mobile/src/components/Statistics.tsx | 4 +-
mobile/src/screens/Home.tsx | 4 +-
mobile/src/screens/IdentityData.tsx | 86 ++++++++++++-------
mobile/src/screens/Volunteer.tsx | 11 +--
mobile/src/services/user/user.api.ts | 19 +++-
mobile/src/services/user/user.service.ts | 4 +-
.../services/volunteer/volunteer.service.ts | 4 +-
10 files changed, 113 insertions(+), 48 deletions(-)
diff --git a/backend/src/api/_mobile/user/presenters/user-personal-data.presenter.ts b/backend/src/api/_mobile/user/presenters/user-personal-data.presenter.ts
index fde70765a..4cbe45df3 100644
--- a/backend/src/api/_mobile/user/presenters/user-personal-data.presenter.ts
+++ b/backend/src/api/_mobile/user/presenters/user-personal-data.presenter.ts
@@ -1,6 +1,9 @@
import { Expose } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
-import { IUserPersonalDataModel } from 'src/modules/user/models/user-personal-data.model';
+import {
+ ILegalGuardian,
+ IUserPersonalDataModel,
+} from 'src/modules/user/models/user-personal-data.model';
export class UserPersonalDataPresenter {
constructor(personalData: IUserPersonalDataModel) {
@@ -11,6 +14,9 @@ export class UserPersonalDataPresenter {
this.identityDocumentExpirationDate =
personalData.identityDocumentExpirationDate;
this.address = personalData.address;
+ this.cnp = personalData.cnp;
+ this.identityDocumentIssuedBy = personalData.identityDocumentIssuedBy;
+ this.legalGuardian = personalData.legalGuardian;
}
@Expose()
@@ -20,6 +26,13 @@ export class UserPersonalDataPresenter {
})
id: string;
+ @Expose()
+ @ApiProperty({
+ description: 'The cnp of the user',
+ example: '1234567890123',
+ })
+ cnp: string;
+
@Expose()
@ApiProperty({
description: 'The identity document series',
@@ -48,4 +61,12 @@ export class UserPersonalDataPresenter {
@Expose()
@ApiProperty({ description: 'The identity document expiration date' })
identityDocumentExpirationDate: Date;
+
+ @Expose()
+ @ApiProperty({ description: 'The identity document issued by' })
+ identityDocumentIssuedBy: string;
+
+ @Expose()
+ @ApiProperty({ description: 'The legal guardian of the user' })
+ legalGuardian: ILegalGuardian;
}
diff --git a/mobile/src/common/interfaces/user-profile.interface.ts b/mobile/src/common/interfaces/user-profile.interface.ts
index adab63285..4dad8f7a7 100644
--- a/mobile/src/common/interfaces/user-profile.interface.ts
+++ b/mobile/src/common/interfaces/user-profile.interface.ts
@@ -5,13 +5,14 @@ import { IOrganizationVolunteer } from './organization-list-item.interface';
export interface IUserPersonalData {
id: string;
- identityDocumentCNP: string;
+ cnp: string;
identityDocumentSeries: string;
identityDocumentNumber: string;
address: string;
identityDocumentIssueDate: Date;
identityDocumentExpirationDate: Date;
identityDocumentIssuedBy: string;
+ legalGuardian?: ILegalGuardianData;
}
export interface ILegalGuardianData {
@@ -37,7 +38,6 @@ export interface IUserProfile {
sex: Sex;
location?: ICity;
userPersonalData: IUserPersonalData;
- legalGuardianData?: ILegalGuardianData;
activeOrganization: IOrganizationVolunteer | null;
myOrganizations: IOrganizationVolunteer[];
profilePicture?: string;
diff --git a/mobile/src/components/LatestNews.tsx b/mobile/src/components/LatestNews.tsx
index 512ec6771..169e7417b 100644
--- a/mobile/src/components/LatestNews.tsx
+++ b/mobile/src/components/LatestNews.tsx
@@ -22,7 +22,7 @@ const LatestNews = ({ navigation, eva }: LatestNewsProps) => {
const { t } = useTranslation('home');
const {
- isFetching: isLoadingAnouncements,
+ isLoading: isLoadingAnouncements,
data: anouncements,
error: getAnouncementsError,
refetch,
diff --git a/mobile/src/components/Statistics.tsx b/mobile/src/components/Statistics.tsx
index 618abd4bf..a84ff70a0 100644
--- a/mobile/src/components/Statistics.tsx
+++ b/mobile/src/components/Statistics.tsx
@@ -19,7 +19,7 @@ const Statistics = ({ navigation }: { navigation: any }) => {
const { userProfile } = useUserProfile();
const {
- isFetching: isFetchingStatistics,
+ isLoading: isLoadingStatistics,
data: statistics,
error: getStatisticsError,
refetch,
@@ -48,7 +48,7 @@ const Statistics = ({ navigation }: { navigation: any }) => {
navigation.navigate('news', { type: NewsType.ORGANIZATIONS });
};
- if (isFetchingStatistics) {
+ if (isLoadingStatistics) {
return ;
}
diff --git a/mobile/src/screens/Home.tsx b/mobile/src/screens/Home.tsx
index 2f0203b55..44ff0360d 100644
--- a/mobile/src/screens/Home.tsx
+++ b/mobile/src/screens/Home.tsx
@@ -25,9 +25,9 @@ const Home = ({ navigation }: any) => {
const { userProfile } = useUserProfile();
const { setUserProfile } = useStore();
- const { isRefetching: isRefetchingMonthlyStatistics, refetch: refetchMonthlyStatistics } =
+ const { isLoading: isRefetchingMonthlyStatistics, refetch: refetchMonthlyStatistics } =
useMonthlyStatistics();
- const { isRefetching: isRefetchingVicStatistics, refetch: refetchVicStatistics } =
+ const { isLoading: isRefetchingVicStatistics, refetch: refetchVicStatistics } =
useVicStatistics();
const onAddVolunteeringHours = () => {
diff --git a/mobile/src/screens/IdentityData.tsx b/mobile/src/screens/IdentityData.tsx
index 5e7ea4396..14c58056e 100644
--- a/mobile/src/screens/IdentityData.tsx
+++ b/mobile/src/screens/IdentityData.tsx
@@ -19,6 +19,7 @@ import { REGEX } from '../common/constants/constants';
import { useUserProfile } from '../store/profile/profile.selector';
import { usePaddingTop } from '../hooks/usePaddingTop';
import { differenceInYears, parseISO } from 'date-fns';
+import { UserPersonalDataPayload } from '../services/user/user.api';
export type IdentityDataFormTypes = {
identityDocumentCNP: string;
@@ -35,6 +36,32 @@ export type IdentityDataFormTypes = {
guardianPhone?: string;
};
+export const mapIdentityDataFormToPayload = (
+ formData: IdentityDataFormTypes,
+): UserPersonalDataPayload => {
+ const payload: UserPersonalDataPayload = {
+ cnp: formData.identityDocumentCNP,
+ identityDocumentSeries: formData.identityDocumentSeries.toUpperCase(),
+ identityDocumentNumber: formData.identityDocumentNumber,
+ address: formData.address,
+ identityDocumentIssueDate: formData.identityDocumentIssueDate,
+ identityDocumentExpirationDate: formData.identityDocumentExpirationDate,
+ identityDocumentIssuedBy: formData.identityDocumentIssuedBy,
+ };
+
+ if (formData.guardianName) {
+ payload.legalGuardian = {
+ name: formData.guardianName,
+ identityDocumentSeries: formData.guardianIdentityDocumentSeries!.toUpperCase(),
+ identityDocumentNumber: formData.guardianIdentityDocumentNumber!,
+ email: formData.guardianEmail!,
+ phone: formData.guardianPhone!,
+ };
+ }
+
+ return payload;
+};
+
const schema = (isUserOver16: boolean, userBirthday: Date | undefined) =>
yup.object({
identityDocumentCNP: yup
@@ -176,8 +203,8 @@ const IdentityData = ({ navigation, route }: any) => {
const [isUserOver16, setIsUserOver16] = useState(
userProfile?.birthday
? isOver16(userProfile?.birthday)
- : userProfile?.userPersonalData.identityDocumentCNP
- ? isOver16FromCNP(userProfile?.userPersonalData.identityDocumentCNP)
+ : userProfile?.userPersonalData.cnp
+ ? isOver16FromCNP(userProfile?.userPersonalData.cnp)
: true,
);
@@ -200,15 +227,20 @@ const IdentityData = ({ navigation, route }: any) => {
// here the data can be null on first user creation
if (userPersonalData) {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { id, ...formData } = userPersonalData;
-
// init form data with the user profile personal data
reset({
- ...formData,
- // TODO: add the new fields
- identityDocumentIssueDate: new Date(formData.identityDocumentIssueDate),
- identityDocumentExpirationDate: new Date(formData.identityDocumentExpirationDate),
+ identityDocumentCNP: userPersonalData.cnp,
+ identityDocumentSeries: userPersonalData.identityDocumentSeries,
+ identityDocumentNumber: userPersonalData.identityDocumentNumber,
+ address: userPersonalData.address,
+ identityDocumentIssueDate: new Date(userPersonalData.identityDocumentIssueDate),
+ identityDocumentExpirationDate: new Date(userPersonalData.identityDocumentExpirationDate),
+ identityDocumentIssuedBy: userPersonalData.identityDocumentIssuedBy,
+ guardianName: userPersonalData.legalGuardian?.name,
+ guardianIdentityDocumentSeries: userPersonalData.legalGuardian?.identityDocumentSeries,
+ guardianIdentityDocumentNumber: userPersonalData.legalGuardian?.identityDocumentNumber,
+ guardianEmail: userPersonalData.legalGuardian?.email,
+ guardianPhone: userPersonalData.legalGuardian?.phone,
});
}
}, [userProfile, reset]);
@@ -231,28 +263,21 @@ const IdentityData = ({ navigation, route }: any) => {
};
const onSubmit = async (payload: IdentityDataFormTypes) => {
- updateUserPersonalData(
- {
- ...payload,
- // TODO: add the new fields
- identityDocumentSeries: payload.identityDocumentSeries.toLocaleUpperCase(),
+ updateUserPersonalData(mapIdentityDataFormToPayload(payload), {
+ onSuccess: () => {
+ // callback in case we are redirected here from any other place than settings screen
+ Toast.show({ type: 'success', text1: `${t('form.submit.success')}` });
+ if (route?.params?.shouldGoBack) {
+ navigation.goBack();
+ }
},
- {
- onSuccess: () => {
- // callback in case we are redirected here from any other place than settings screen
- Toast.show({ type: 'success', text1: `${t('form.submit.success')}` });
- if (route?.params?.shouldGoBack) {
- navigation.goBack();
- }
- },
- onError: (error: any) => {
- Toast.show({
- type: 'error',
- text1: `${InternalErrors.USER_ERRORS.getError(error.response?.data.code_error)}`,
- });
- },
+ onError: (error: any) => {
+ Toast.show({
+ type: 'error',
+ text1: `${InternalErrors.USER_ERRORS.getError(error.response?.data.code_error)}`,
+ });
},
- );
+ });
};
return (
@@ -284,6 +309,7 @@ const IdentityData = ({ navigation, route }: any) => {
error={errors.identityDocumentSeries}
placeholder={t('form.series.placeholder')}
disabled={isUpdateingPersonalData}
+ autoCapitalize="characters"
/>
{
error={errors.guardianIdentityDocumentSeries}
placeholder={t('form.guardian.series.placeholder')}
disabled={isUpdateingPersonalData}
+ autoCapitalize="characters"
/>
{
name="guardianIdentityDocumentNumber"
error={errors.guardianIdentityDocumentNumber}
placeholder={t('form.guardian.number.placeholder')}
+ keyboardType="phone-pad"
disabled={isUpdateingPersonalData}
/>
{
const {
data: stats,
- isFetching: isLoadingStats,
+ isLoading: isLoadingStats,
refetch,
- } = useVolunteerStats(userProfile?.activeOrganization?.volunteerId as string);
+ } = useVolunteerStats(userProfile?.activeOrganization?.volunteerId);
useFocusEffect(
React.useCallback(() => {
refetchUserProfile();
- refetch();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []),
+ if (userProfile?.activeOrganization?.volunteerId) {
+ refetch();
+ }
+ }, [refetch, refetchUserProfile, userProfile?.activeOrganization?.volunteerId]),
);
const onViewOrganizationButtonPress = () => {
diff --git a/mobile/src/services/user/user.api.ts b/mobile/src/services/user/user.api.ts
index a966d94f2..79c5ce262 100644
--- a/mobile/src/services/user/user.api.ts
+++ b/mobile/src/services/user/user.api.ts
@@ -1,7 +1,6 @@
import API from '../api';
import { IUserProfile } from '../../common/interfaces/user-profile.interface';
import { ICreateUserPayload } from '../../common/interfaces/create-user-payload.interface';
-import { IdentityDataFormTypes } from '../../screens/IdentityData';
import { AccountDataFormTypes } from '../../screens/AccountData';
import { ImageAttachement } from '../../common/interfaces/image-attachement.interface';
@@ -13,8 +12,24 @@ export const getUserProfile = async (): Promise => {
return API.get('/mobile/user/profile').then((res) => res.data);
};
+export interface UserPersonalDataPayload {
+ cnp: string;
+ identityDocumentSeries: string;
+ identityDocumentNumber: string;
+ address: string;
+ identityDocumentIssueDate: Date;
+ identityDocumentExpirationDate: Date;
+ identityDocumentIssuedBy: string;
+ legalGuardian?: {
+ name: string;
+ identityDocumentSeries: string;
+ identityDocumentNumber: string;
+ email: string;
+ phone: string;
+ };
+}
export const updateUserPersonalData = async (
- updates: IdentityDataFormTypes,
+ updates: UserPersonalDataPayload,
): Promise => {
return API.patch('/mobile/user/personal-data', updates).then((res) => res.data);
};
diff --git a/mobile/src/services/user/user.service.ts b/mobile/src/services/user/user.service.ts
index e2f82afa2..634fb24c4 100644
--- a/mobile/src/services/user/user.service.ts
+++ b/mobile/src/services/user/user.service.ts
@@ -5,9 +5,9 @@ import {
getUserProfile,
updateUserPersonalData,
updateUserProfile,
+ UserPersonalDataPayload,
} from './user.api';
import { ICreateUserPayload } from '../../common/interfaces/create-user-payload.interface';
-import { IdentityDataFormTypes } from '../../screens/IdentityData';
import { AccountDataFormTypes } from '../../screens/AccountData';
import { ImageAttachement } from '../../common/interfaces/image-attachement.interface';
import { IUserProfile } from '../../common/interfaces/user-profile.interface';
@@ -48,7 +48,7 @@ export const useUpdateUserPersonalDataMutation = () => {
const { setIdentityData } = useStore();
return useMutation(
['personal-data'],
- (personalData: IdentityDataFormTypes) => updateUserPersonalData(personalData),
+ (personalData: UserPersonalDataPayload) => updateUserPersonalData(personalData),
{ onSuccess: (data: IUserProfile) => setIdentityData(data.userPersonalData) },
);
};
diff --git a/mobile/src/services/volunteer/volunteer.service.ts b/mobile/src/services/volunteer/volunteer.service.ts
index 23dba7d98..526878a74 100644
--- a/mobile/src/services/volunteer/volunteer.service.ts
+++ b/mobile/src/services/volunteer/volunteer.service.ts
@@ -73,8 +73,8 @@ export const useVolunteerProfile = (organizationId: string) => {
);
};
-export const useVolunteerStats = (volunteerId: string) => {
- return useQuery(['volunteer-stats', volunteerId], () => getVolunteerStats(volunteerId), {
+export const useVolunteerStats = (volunteerId: string | undefined) => {
+ return useQuery(['volunteer-stats', volunteerId], () => getVolunteerStats(volunteerId!), {
enabled: !!volunteerId,
});
};
From 701cb86db40c0687bad80536b26580adf44dc700 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Tue, 10 Sep 2024 15:15:00 +0300
Subject: [PATCH 24/55] feat: [Contracts] Include CNP and Address for legal
guardian
---
.../user/dto/update-user-personal-data.dto.ts | 11 +++
.../user/models/user-personal-data.model.ts | 2 +
mobile/src/assets/locales/en/translation.json | 17 +++-
mobile/src/assets/locales/ro/translation.json | 17 +++-
.../interfaces/user-profile.interface.ts | 2 +
mobile/src/screens/IdentityData.tsx | 80 ++++++++++++++-----
mobile/src/services/user/user.api.ts | 10 +--
7 files changed, 110 insertions(+), 29 deletions(-)
diff --git a/backend/src/api/_mobile/user/dto/update-user-personal-data.dto.ts b/backend/src/api/_mobile/user/dto/update-user-personal-data.dto.ts
index ddb05098e..eb4e7ecae 100644
--- a/backend/src/api/_mobile/user/dto/update-user-personal-data.dto.ts
+++ b/backend/src/api/_mobile/user/dto/update-user-personal-data.dto.ts
@@ -18,6 +18,17 @@ class LegalGuardianDto {
@MaxLength(100)
name: string;
+ @IsString()
+ @IsNotEmpty()
+ @Length(13)
+ cnp: string;
+
+ @IsString()
+ @IsNotEmpty()
+ @MinLength(2)
+ @MaxLength(100)
+ address: string;
+
@IsString()
@IsNotEmpty()
@Length(2)
diff --git a/backend/src/modules/user/models/user-personal-data.model.ts b/backend/src/modules/user/models/user-personal-data.model.ts
index 3e9393783..f2413773e 100644
--- a/backend/src/modules/user/models/user-personal-data.model.ts
+++ b/backend/src/modules/user/models/user-personal-data.model.ts
@@ -14,6 +14,8 @@ export interface IUserPersonalDataModel {
export interface ILegalGuardian {
name: string;
+ cnp: string;
+ address: string;
identityDocumentSeries: string;
identityDocumentNumber: string;
email: string;
diff --git a/mobile/src/assets/locales/en/translation.json b/mobile/src/assets/locales/en/translation.json
index 3f458b708..12bcec03a 100644
--- a/mobile/src/assets/locales/en/translation.json
+++ b/mobile/src/assets/locales/en/translation.json
@@ -283,6 +283,21 @@
"placeholder": "John Doe",
"required": "Legal guardian's full name is mandatory"
},
+ "cnp": {
+ "label": "CNP",
+ "placeholder": "Enter CNP",
+ "matches": "CNP must contain only digits",
+ "required": "CNP is mandatory",
+ "length": "CNP must contain {{number}} digits"
+ },
+ "address": {
+ "label": "Address",
+ "placeholder": "Enter address",
+ "required": "Address is mandatory",
+ "helper": "Enter the full address from your ID.",
+ "min": "Address can have a minimum of {{value}} characters",
+ "max": "Address can have a maximum of {{value}} characters"
+ },
"series": {
"label": "Identity document (ID) series",
"matches": "The series must contain only letters",
@@ -827,4 +842,4 @@
"confirm": "Confirm Deletion",
"error": "There was an error trying to delete your account. Please contact us at {{value}} to finalize the process."
}
-}
\ No newline at end of file
+}
diff --git a/mobile/src/assets/locales/ro/translation.json b/mobile/src/assets/locales/ro/translation.json
index 020d39773..55bab3f04 100644
--- a/mobile/src/assets/locales/ro/translation.json
+++ b/mobile/src/assets/locales/ro/translation.json
@@ -283,6 +283,21 @@
"placeholder": "Ion Popescu",
"required": "Numele și prenumele tutorelui legal sunt obligatorii"
},
+ "cnp": {
+ "label": "CNP",
+ "placeholder": "Introdu CNP-ul",
+ "matches": "CNP-ul trebuie să conțină doar cifre",
+ "required": "CNP-ul este obligatoriu",
+ "length": "CNP-ul trebuie să conțină {{number}} cifre"
+ },
+ "address": {
+ "label": "Domiciliat în",
+ "placeholder": "Introdu adresa",
+ "required": "Domiciliul este obligatoriu",
+ "helper": "Introdu adresa completa din buletin.",
+ "min": "Domiciliul poate să aibă minim {{value}} caractere",
+ "max": "Domiciliul poate să aibă maxim {{value}} caractere"
+ },
"series": {
"label": "Serie document identitate (CI)",
"matches": "Seria trebuie să conțină doar litere",
@@ -828,4 +843,4 @@
"confirm": "Confirmă Ștergerea",
"error": "A apărut o eroare la ștergerea contului. Contactează-ne la adresa de e-mail {{value}}, și te vom ajuta să finalizezi procesul."
}
-}
\ No newline at end of file
+}
diff --git a/mobile/src/common/interfaces/user-profile.interface.ts b/mobile/src/common/interfaces/user-profile.interface.ts
index 4dad8f7a7..fb7360ace 100644
--- a/mobile/src/common/interfaces/user-profile.interface.ts
+++ b/mobile/src/common/interfaces/user-profile.interface.ts
@@ -17,6 +17,8 @@ export interface IUserPersonalData {
export interface ILegalGuardianData {
name: string;
+ cnp: string;
+ address: string;
identityDocumentSeries: string;
identityDocumentNumber: string;
email: string;
diff --git a/mobile/src/screens/IdentityData.tsx b/mobile/src/screens/IdentityData.tsx
index 14c58056e..14d17d0a9 100644
--- a/mobile/src/screens/IdentityData.tsx
+++ b/mobile/src/screens/IdentityData.tsx
@@ -34,6 +34,8 @@ export type IdentityDataFormTypes = {
guardianIdentityDocumentNumber?: string;
guardianEmail?: string;
guardianPhone?: string;
+ guardianCNP?: string;
+ guardianAddress?: string;
};
export const mapIdentityDataFormToPayload = (
@@ -56,13 +58,15 @@ export const mapIdentityDataFormToPayload = (
identityDocumentNumber: formData.guardianIdentityDocumentNumber!,
email: formData.guardianEmail!,
phone: formData.guardianPhone!,
+ cnp: formData.guardianCNP!,
+ address: formData.guardianAddress!,
};
}
return payload;
};
-const schema = (isUserOver16: boolean, userBirthday: Date | undefined) =>
+const formSchema = (isUserOver16: boolean, userBirthday: Date | undefined) =>
yup.object({
identityDocumentCNP: yup
.string()
@@ -113,6 +117,26 @@ const schema = (isUserOver16: boolean, userBirthday: Date | undefined) =>
then: (schema) => schema.required(`${i18n.t('identity_data:form.guardian.name.required')}`),
otherwise: (schema) => schema.optional(),
}),
+ guardianCNP: yup
+ .string()
+ .matches(REGEX.NUMBERS_ONLY, `${i18n.t('identity_data:form.cnp.matches')}`)
+ .length(13, `${i18n.t('identity_data:form.guardian.cnp.length', { number: 13 })}`)
+ .when([], {
+ is: () => !isUserOver16,
+ then: (schema) => schema.required(`${i18n.t('identity_data:form.guardian.cnp.required')}`),
+ otherwise: (schema) => schema.optional(),
+ }),
+ guardianAddress: yup
+ .string()
+
+ .min(2, `${i18n.t('identity_data:form.guardian.address.min', { value: 2 })}`)
+ .max(100, `${i18n.t('identity_data:form.guardian.address.max', { value: 100 })}`)
+ .when([], {
+ is: () => !isUserOver16,
+ then: (schema) =>
+ schema.required(`${i18n.t('identity_data:form.guardian.address.required')}`),
+ otherwise: (schema) => schema.optional(),
+ }),
guardianIdentityDocumentSeries: yup
.string()
.matches(REGEX.STRINGS_ONLY, `${i18n.t('identity_data:form.guardian.series.matches')}`)
@@ -145,7 +169,7 @@ const schema = (isUserOver16: boolean, userBirthday: Date | undefined) =>
guardianPhone: yup
.string()
.matches(REGEX.NUMBERS_ONLY, `${i18n.t('identity_data:form.guardian.phone.matches')}`)
- .length(9, `${i18n.t('identity_data:form.guardian.phone.length', { number: 10 })}`)
+ .length(10, `${i18n.t('identity_data:form.guardian.phone.length', { number: 10 })}`)
.when([], {
is: () => !isUserOver16,
then: (schema) =>
@@ -219,7 +243,7 @@ const IdentityData = ({ navigation, route }: any) => {
} = useForm({
mode: 'onSubmit',
reValidateMode: 'onChange',
- resolver: yupResolver(schema(isUserOver16, userProfile?.birthday)),
+ resolver: yupResolver(formSchema(isUserOver16, userProfile?.birthday)),
});
useEffect(() => {
@@ -241,6 +265,8 @@ const IdentityData = ({ navigation, route }: any) => {
guardianIdentityDocumentNumber: userPersonalData.legalGuardian?.identityDocumentNumber,
guardianEmail: userPersonalData.legalGuardian?.email,
guardianPhone: userPersonalData.legalGuardian?.phone,
+ guardianAddress: userPersonalData.legalGuardian?.address,
+ guardianCNP: userPersonalData.legalGuardian?.cnp,
});
}
}, [userProfile, reset]);
@@ -366,6 +392,38 @@ const IdentityData = ({ navigation, route }: any) => {
placeholder={t('form.guardian.name.placeholder')}
disabled={isUpdateingPersonalData}
/>
+
+
+
+
{
keyboardType="phone-pad"
disabled={isUpdateingPersonalData}
/>
-
-
>
)}
diff --git a/mobile/src/services/user/user.api.ts b/mobile/src/services/user/user.api.ts
index 79c5ce262..c1ef9aa7b 100644
--- a/mobile/src/services/user/user.api.ts
+++ b/mobile/src/services/user/user.api.ts
@@ -1,5 +1,5 @@
import API from '../api';
-import { IUserProfile } from '../../common/interfaces/user-profile.interface';
+import { ILegalGuardianData, IUserProfile } from '../../common/interfaces/user-profile.interface';
import { ICreateUserPayload } from '../../common/interfaces/create-user-payload.interface';
import { AccountDataFormTypes } from '../../screens/AccountData';
import { ImageAttachement } from '../../common/interfaces/image-attachement.interface';
@@ -20,13 +20,7 @@ export interface UserPersonalDataPayload {
identityDocumentIssueDate: Date;
identityDocumentExpirationDate: Date;
identityDocumentIssuedBy: string;
- legalGuardian?: {
- name: string;
- identityDocumentSeries: string;
- identityDocumentNumber: string;
- email: string;
- phone: string;
- };
+ legalGuardian?: ILegalGuardianData;
}
export const updateUserPersonalData = async (
updates: UserPersonalDataPayload,
From 3a356992ad933bfbb4da0e662bd7a8a894546967 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Wed, 11 Sep 2024 10:57:30 +0300
Subject: [PATCH 25/55] feat: [Contracts] Add user personal data on volunteer
API return data
---
.../src/api/auth/presenters/user.presenter.ts | 8 +++++++
.../repositories/volunteer.repository.ts | 5 ++++
.../src/common/interfaces/user.interface.ts | 24 +++++++++++++++++++
3 files changed, 37 insertions(+)
diff --git a/backend/src/api/auth/presenters/user.presenter.ts b/backend/src/api/auth/presenters/user.presenter.ts
index 9e0fdce15..95ff7e354 100644
--- a/backend/src/api/auth/presenters/user.presenter.ts
+++ b/backend/src/api/auth/presenters/user.presenter.ts
@@ -1,6 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { differenceInYears } from 'date-fns';
+import { UserPersonalDataPresenter } from 'src/api/_mobile/user/presenters/user-personal-data.presenter';
import { CityPresenter } from 'src/api/location/presenters/city.presenter';
import { SEX } from 'src/modules/user/enums/user.enum';
import { IRegularUserModel } from 'src/modules/user/models/regular-user.model';
@@ -15,6 +16,9 @@ export class RegularUserPresenter {
this.sex = user.sex;
this.profilePicture = user.profilePicture;
this.location = user.location ? new CityPresenter(user.location) : null;
+ this.userPersonalData = user.userPersonalData
+ ? new UserPersonalDataPresenter(user.userPersonalData)
+ : null;
}
@Expose()
@@ -52,6 +56,10 @@ export class RegularUserPresenter {
@ApiProperty({ description: 'The users location' })
location: CityPresenter;
+ @Expose()
+ @ApiProperty({ description: 'The user personal data' })
+ userPersonalData?: UserPersonalDataPresenter;
+
private calculateAge = (birthday: Date): number => {
return birthday ? differenceInYears(new Date(), new Date(birthday)) : 0;
};
diff --git a/backend/src/modules/volunteer/repositories/volunteer.repository.ts b/backend/src/modules/volunteer/repositories/volunteer.repository.ts
index 093478b6c..8ce237c08 100644
--- a/backend/src/modules/volunteer/repositories/volunteer.repository.ts
+++ b/backend/src/modules/volunteer/repositories/volunteer.repository.ts
@@ -92,6 +92,11 @@ export class VolunteerRepositoryService
'department',
)
.leftJoinAndMapOne('volunteer.user', 'volunteer.user', 'user')
+ .leftJoinAndMapOne(
+ 'user.userPersonalData',
+ 'user.userPersonalData',
+ 'userPersonalData',
+ )
.leftJoinAndMapOne('user.location', 'user.location', 'location')
.leftJoinAndMapOne('location.county', 'location.county', 'county')
.leftJoinAndMapOne(
diff --git a/frontend/src/common/interfaces/user.interface.ts b/frontend/src/common/interfaces/user.interface.ts
index 88491f028..9ab599e0a 100644
--- a/frontend/src/common/interfaces/user.interface.ts
+++ b/frontend/src/common/interfaces/user.interface.ts
@@ -1,6 +1,28 @@
import { Sex } from '../enums/sex.enum';
import { ICity } from './city.interface';
+export interface IUserPersonalDataModel {
+ id: string;
+ cnp: string;
+ address: string;
+ identityDocumentSeries: string;
+ identityDocumentNumber: string;
+ identityDocumentIssueDate: Date;
+ identityDocumentExpirationDate: Date;
+ identityDocumentIssuedBy: string;
+ legalGuardian?: LegalGuardianIdentityData;
+}
+
+export interface LegalGuardianIdentityData {
+ name: string;
+ cnp: string;
+ address: string;
+ identityDocumentSeries: string;
+ identityDocumentNumber: string;
+ email: string;
+ phone: string;
+}
+
export interface IUser {
id: string;
name: string;
@@ -12,4 +34,6 @@ export interface IUser {
sex: Sex;
createdOn: Date;
updatedOn: Date;
+
+ userPersonalData?: IUserPersonalDataModel;
}
From b3c99092d3fb219ea6a611c73849fb67872424d7 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Wed, 11 Sep 2024 11:11:32 +0300
Subject: [PATCH 26/55] feat: [Contracts] Validate volunteers' legal guardian
data, remove tutorData from contract db table
---
.../user-personal-data.presenter.ts | 4 +-
.../_mobile/volunteer/volunteer.controller.ts | 10 ++-
.../1726041710983-RemoveVolunteerTutorData.ts | 19 ++++++
.../entities/document-contract.entity.ts | 7 +-
.../models/document-contract.model.ts | 20 ++----
.../entities/user-personal-data.entity.ts | 4 +-
.../user/models/user-personal-data.model.ts | 4 +-
.../create-document-contract.usecase.ts | 68 +++++++++++++------
8 files changed, 83 insertions(+), 53 deletions(-)
create mode 100644 backend/src/migrations/1726041710983-RemoveVolunteerTutorData.ts
diff --git a/backend/src/api/_mobile/user/presenters/user-personal-data.presenter.ts b/backend/src/api/_mobile/user/presenters/user-personal-data.presenter.ts
index 4cbe45df3..117fa443b 100644
--- a/backend/src/api/_mobile/user/presenters/user-personal-data.presenter.ts
+++ b/backend/src/api/_mobile/user/presenters/user-personal-data.presenter.ts
@@ -1,7 +1,7 @@
import { Expose } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import {
- ILegalGuardian,
+ LegalGuardianIdentityData,
IUserPersonalDataModel,
} from 'src/modules/user/models/user-personal-data.model';
@@ -68,5 +68,5 @@ export class UserPersonalDataPresenter {
@Expose()
@ApiProperty({ description: 'The legal guardian of the user' })
- legalGuardian: ILegalGuardian;
+ legalGuardian: LegalGuardianIdentityData;
}
diff --git a/backend/src/api/_mobile/volunteer/volunteer.controller.ts b/backend/src/api/_mobile/volunteer/volunteer.controller.ts
index 687873cfc..1df02df11 100644
--- a/backend/src/api/_mobile/volunteer/volunteer.controller.ts
+++ b/backend/src/api/_mobile/volunteer/volunteer.controller.ts
@@ -35,9 +35,8 @@ export class MobileVolunteerController {
async getVolunteerProfile(
@Param('id', UuidValidationPipe) volunteerId: string,
): Promise {
- const volunteer = await this.getVolunteerProfileUsecase.execute(
- volunteerId,
- );
+ const volunteer =
+ await this.getVolunteerProfileUsecase.execute(volunteerId);
return new VolunteerPresenter(volunteer);
}
@@ -46,9 +45,8 @@ export class MobileVolunteerController {
async getVolunteerOrganizationStats(
@Param('id', UuidValidationPipe) volunteerId: string,
): Promise {
- const volunteer = await this.getVolunteerOrganizationStatusUsecase.execute(
- volunteerId,
- );
+ const volunteer =
+ await this.getVolunteerOrganizationStatusUsecase.execute(volunteerId);
return new VolunteerStatsPresenter(volunteer);
}
diff --git a/backend/src/migrations/1726041710983-RemoveVolunteerTutorData.ts b/backend/src/migrations/1726041710983-RemoveVolunteerTutorData.ts
new file mode 100644
index 000000000..efebc5874
--- /dev/null
+++ b/backend/src/migrations/1726041710983-RemoveVolunteerTutorData.ts
@@ -0,0 +1,19 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class RemoveVolunteerTutorData1726041710983
+ implements MigrationInterface
+{
+ name = 'RemoveVolunteerTutorData1726041710983';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP COLUMN "volunteer_tutor_data"`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD "volunteer_tutor_data" jsonb`,
+ );
+ }
+}
diff --git a/backend/src/modules/documents/entities/document-contract.entity.ts b/backend/src/modules/documents/entities/document-contract.entity.ts
index 4fdf1ebf3..8cb85feba 100644
--- a/backend/src/modules/documents/entities/document-contract.entity.ts
+++ b/backend/src/modules/documents/entities/document-contract.entity.ts
@@ -11,8 +11,8 @@ import { DocumentTemplateEntity } from './document-template.entity';
import { AdminUserEntity } from 'src/modules/user/entities/user.entity';
import { VolunteerEntity } from 'src/modules/volunteer/entities/volunteer.entity';
import { BaseEntity } from 'src/infrastructure/base/base-entity';
-import { PersonalData } from '../models/document-contract.model';
import { DocumentSignatureEntity } from './document-signature.entity';
+import { VolunteerContractIdentityData } from '../models/document-contract.model';
@Entity({ name: 'document_contract' })
export class DocumentContractEntity extends BaseEntity {
@@ -45,10 +45,7 @@ export class DocumentContractEntity extends BaseEntity {
// ==================== VOLUNTEER RELATION =================================
@Column({ type: 'jsonb', name: 'volunteer_data', nullable: false })
- volunteerData: PersonalData;
-
- @Column({ type: 'jsonb', name: 'volunteer_tutor_data', nullable: true })
- volunteerTutorData: PersonalData;
+ volunteerData: VolunteerContractIdentityData;
@Column({
type: 'varchar',
diff --git a/backend/src/modules/documents/models/document-contract.model.ts b/backend/src/modules/documents/models/document-contract.model.ts
index 62ce19776..3ccb8f894 100644
--- a/backend/src/modules/documents/models/document-contract.model.ts
+++ b/backend/src/modules/documents/models/document-contract.model.ts
@@ -13,17 +13,11 @@ import {
IVolunteerModel,
VolunteerModelTransformer,
} from 'src/modules/volunteer/model/volunteer.model';
+import { IUserPersonalDataModel } from 'src/modules/user/models/user-personal-data.model';
-// TODO: Change this with the existent IUserPersonalDataModel after is updated
-export interface PersonalData {
- CNP: string;
+export type VolunteerContractIdentityData = IUserPersonalDataModel & {
name: string;
- address: string;
- identityDocumentSeries: string;
- identityDocumentNumber: string;
- identityDocumentIssuedBy: string;
- identityDocumentIssuedDate: Date;
-}
+};
export interface IDocumentContractModel extends IBaseModel {
id: string;
@@ -48,8 +42,7 @@ export interface IDocumentContractModel extends IBaseModel {
volunteerId: string;
volunteer: IVolunteerModel;
- volunteerData: PersonalData; // TODO: le tragem din user in usecase-ul de creare contract si raman ca snapshoot
- volunteerTutorData?: PersonalData; // TODO: le tragem din user in usecase-ul de creare contract si raman ca snapshoot
+ volunteerData: VolunteerContractIdentityData; // TODO: le tragem din user in usecase-ul de creare contract si raman ca snapshoot
filePath?: string;
@@ -66,8 +59,7 @@ export type CreateDocumentContractOptions = {
documentStartDate: Date;
documentEndDate: Date;
- volunteerData: PersonalData;
- volunteerTutorData?: PersonalData;
+ volunteerData: VolunteerContractIdentityData;
volunteerId: string;
organizationId: string;
@@ -105,7 +97,6 @@ export class DocumentContractTransformer {
volunteerId: entity.volunteerId,
volunteer: VolunteerModelTransformer.fromEntity(entity.volunteer),
volunteerData: entity.volunteerData,
- volunteerTutorData: entity.volunteerTutorData,
// Template
documentTemplateId: entity.documentTemplateId,
documentTemplate: DocumentTemplateTransformer.fromEntity(
@@ -131,7 +122,6 @@ export class DocumentContractTransformer {
entity.documentEndDate = model.documentEndDate;
entity.volunteerId = model.volunteerId;
entity.volunteerData = model.volunteerData;
- entity.volunteerTutorData = model.volunteerTutorData;
entity.organizationId = model.organizationId;
entity.documentTemplateId = model.documentTemplateId;
entity.createdByAdminId = model.createdByAdminId;
diff --git a/backend/src/modules/user/entities/user-personal-data.entity.ts b/backend/src/modules/user/entities/user-personal-data.entity.ts
index 2acf2d2aa..31fd38077 100644
--- a/backend/src/modules/user/entities/user-personal-data.entity.ts
+++ b/backend/src/modules/user/entities/user-personal-data.entity.ts
@@ -1,6 +1,6 @@
import { BaseEntity } from 'src/infrastructure/base/base-entity';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
-import { ILegalGuardian } from '../models/user-personal-data.model';
+import { LegalGuardianIdentityData } from '../models/user-personal-data.model';
@Entity({ name: 'user_personal_data' })
export class UserPersonalDataEntity extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
@@ -31,5 +31,5 @@ export class UserPersonalDataEntity extends BaseEntity {
identityDocumentIssuedBy: string;
@Column({ type: 'jsonb', name: 'legal_guardian', nullable: true })
- legalGuardian: ILegalGuardian;
+ legalGuardian: LegalGuardianIdentityData;
}
diff --git a/backend/src/modules/user/models/user-personal-data.model.ts b/backend/src/modules/user/models/user-personal-data.model.ts
index f2413773e..eac225cdf 100644
--- a/backend/src/modules/user/models/user-personal-data.model.ts
+++ b/backend/src/modules/user/models/user-personal-data.model.ts
@@ -9,10 +9,10 @@ export interface IUserPersonalDataModel {
identityDocumentIssueDate: Date;
identityDocumentExpirationDate: Date;
identityDocumentIssuedBy: string;
- legalGuardian?: ILegalGuardian;
+ legalGuardian?: LegalGuardianIdentityData;
}
-export interface ILegalGuardian {
+export interface LegalGuardianIdentityData {
name: string;
cnp: string;
address: string;
diff --git a/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts b/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts
index c1f6926ff..6e4d7a957 100644
--- a/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts
+++ b/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts
@@ -4,7 +4,10 @@ import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.serv
import { CreateDocumentContractOptions } from 'src/modules/documents/models/document-contract.model';
import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade';
import { DocumentTemplateFacade } from 'src/modules/documents/services/document-template.facade';
-import { IUserPersonalDataModel } from 'src/modules/user/models/user-personal-data.model';
+import {
+ IUserPersonalDataModel,
+ LegalGuardianIdentityData,
+} from 'src/modules/user/models/user-personal-data.model';
import { VolunteerStatus } from 'src/modules/volunteer/enums/volunteer-status.enum';
import { VolunteerExceptionMessages } from 'src/modules/volunteer/exceptions/volunteer.exceptions';
import { IVolunteerModel } from 'src/modules/volunteer/model/volunteer.model';
@@ -63,30 +66,17 @@ export class CreateDocumentContractUsecase implements IUseCaseService {
// 6. Extract volunteerData and volunteerTutorData from the user
const volunteerPersonalData = volunteer.user.userPersonalData;
+
+ console.log(volunteerPersonalData);
+
await this.validateVolunteerPersonalData(volunteerPersonalData);
- // TODO: 6.1. Extract volunteerTutorData from the user and validate it
+ await this.validateLegalGuardianData(volunteerPersonalData.legalGuardian);
const newContractOptions: CreateDocumentContractOptions = {
...newContract,
volunteerData: {
name: volunteer.user.name,
- CNP: '11321321312321 MISSING',
- address: volunteerPersonalData.address,
- identityDocumentSeries: volunteerPersonalData.identityDocumentSeries,
- identityDocumentNumber: volunteerPersonalData.identityDocumentNumber,
- // identityDocumentIssuedBy: volunteerPersonalData.identityDocumentIssuedBy,
- // identityDocumentIssuedDate: volunteerPersonalData.identityDocumentIssuedDate,
- identityDocumentIssuedBy: 'Missing',
- identityDocumentIssuedDate: new Date(),
- },
- volunteerTutorData: {
- CNP: '11321321312321',
- address: 'Str. Unirii 59',
- name: 'Test',
- identityDocumentSeries: 'AB',
- identityDocumentNumber: '1234567890',
- identityDocumentIssuedBy: 'Comisaria',
- identityDocumentIssuedDate: new Date(),
+ ...volunteerPersonalData,
},
};
@@ -114,7 +104,7 @@ export class CreateDocumentContractUsecase implements IUseCaseService {
volunteerPersonalData: IUserPersonalDataModel,
): Promise {
const personalDataSchema = z.object({
- CNP: z.string().length(13, 'CNP must be 13 digits'),
+ cnp: z.string().length(13, 'CNP must be 13 digits'),
address: z.string().min(1, 'Address is required'),
identityDocumentSeries: z
.string()
@@ -125,7 +115,7 @@ export class CreateDocumentContractUsecase implements IUseCaseService {
identityDocumentIssuedBy: z
.string()
.min(1, 'Identity document issuer is required'),
- identityDocumentIssuedDate: z
+ identityDocumentIssueDate: z.coerce
.date()
.max(new Date(), 'Issue date cannot be in the future'),
});
@@ -149,6 +139,42 @@ export class CreateDocumentContractUsecase implements IUseCaseService {
}
}
+ private async validateLegalGuardianData(
+ legalGuardianData: LegalGuardianIdentityData,
+ ): Promise {
+ const legalGuardianSchema = z.object({
+ name: z.string().min(1, 'Name is required'),
+ cnp: z.string().length(13, 'CNP must be 13 digits'),
+ address: z.string().min(1, 'Address is required'),
+ identityDocumentSeries: z
+ .string()
+ .min(2, 'Identity document series is required'),
+ identityDocumentNumber: z
+ .string()
+ .min(1, 'Identity document number is required'),
+ email: z.string().email('Invalid email address'),
+ phone: z.string().min(1, 'Phone number is required'),
+ });
+
+ try {
+ legalGuardianSchema.parse(legalGuardianData);
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ const invalidFields = error.issues.map((issue) => ({
+ field: issue.path.join('.'),
+ message: issue.message,
+ }));
+
+ this.exceptionsService.badRequestException({
+ message: `Invalid legal guardian data ${JSON.stringify(invalidFields)}`,
+ code_error: 'INVALID_LEGAL_GUARDIAN_DATA',
+ });
+ } else {
+ throw error; // Re-throw unexpected errors
+ }
+ }
+ }
+
private async checkVolunteerExists(
volunteerId: string,
organizationId: string,
From 1cb830dc54ebb9350d36f8260eeed04f16b01b62 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Wed, 11 Sep 2024 12:44:32 +0300
Subject: [PATCH 27/55] feat: [Contracts] API to retrieve many templates as per
design structure
---
.../documents/document-template.controller.ts | 35 +-
.../dto/get-many-document-templates.dto.ts | 3 +
...ument-template-list-view-item.presenter.ts | 64 +++
...26046690797-DDLDocumentTemplateListView.ts | 46 ++
.../src/modules/documents/documents.module.ts | 4 +
.../document-template-list-view.entity.ts | 48 ++
.../document-template-list-view.model.ts | 30 ++
.../document-template-list-view.repository.ts | 64 +++
.../services/document-template.facade.ts | 13 +
.../get-many-document-templates.usecase.ts | 23 +
backend/src/usecases/use-case.module.ts | 462 ++++++------------
11 files changed, 486 insertions(+), 306 deletions(-)
create mode 100644 backend/src/api/documents/dto/get-many-document-templates.dto.ts
create mode 100644 backend/src/api/documents/presenters/document-template-list-view-item.presenter.ts
create mode 100644 backend/src/migrations/1726046690797-DDLDocumentTemplateListView.ts
create mode 100644 backend/src/modules/documents/entities/document-template-list-view.entity.ts
create mode 100644 backend/src/modules/documents/models/document-template-list-view.model.ts
create mode 100644 backend/src/modules/documents/repositories/document-template-list-view.repository.ts
create mode 100644 backend/src/usecases/documents/new_contracts/get-many-document-templates.usecase.ts
diff --git a/backend/src/api/documents/document-template.controller.ts b/backend/src/api/documents/document-template.controller.ts
index e28aa12d2..ef171ff61 100644
--- a/backend/src/api/documents/document-template.controller.ts
+++ b/backend/src/api/documents/document-template.controller.ts
@@ -1,4 +1,12 @@
-import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
+import {
+ Body,
+ Controller,
+ Get,
+ Param,
+ Post,
+ Query,
+ 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';
@@ -8,6 +16,10 @@ import { UuidValidationPipe } from 'src/infrastructure/pipes/uuid.pipe';
import { WebJwtAuthGuard } from 'src/modules/auth/guards/jwt-web.guard';
import { CreateDocumentTemplateUsecase } from 'src/usecases/documents/new_contracts/create-document-template.usecase';
import { GetOneDocumentTemplateUseCase } from 'src/usecases/documents/new_contracts/get-one-document-template.usecase';
+import { GetManyDocumentTemplatesDto } from './dto/get-many-document-templates.dto';
+import { DocumentTemplateListViewItemPresenter } from './presenters/document-template-list-view-item.presenter';
+import { GetManyDocumentTemplatesUsecase } from 'src/usecases/documents/new_contracts/get-many-document-templates.usecase';
+import { PaginatedPresenter } from 'src/infrastructure/presenters/generic-paginated.presenter';
@ApiBearerAuth()
@UseGuards(WebJwtAuthGuard)
@@ -16,6 +28,7 @@ export class DocumentTemplateController {
constructor(
private readonly createDocumentTemplateUsecase: CreateDocumentTemplateUsecase,
private readonly getOneDocumentTemplateUsecase: GetOneDocumentTemplateUseCase,
+ private readonly getManyDocumentTemplatesUsecase: GetManyDocumentTemplatesUsecase,
) {}
@ApiBody({ type: CreateDocumentTemplateDto })
@@ -46,4 +59,24 @@ export class DocumentTemplateController {
return new DocumentTemplatePresenter(documentTemplate);
}
+
+ @Get()
+ async getMany(
+ @Query() query: GetManyDocumentTemplatesDto,
+ @ExtractUser() { organizationId }: IAdminUserModel,
+ ): Promise> {
+ const documentTemplates =
+ await this.getManyDocumentTemplatesUsecase.execute({
+ ...query,
+ organizationId,
+ });
+
+ return new PaginatedPresenter({
+ ...documentTemplates,
+ items: documentTemplates.items.map(
+ (documentTemplate) =>
+ new DocumentTemplateListViewItemPresenter(documentTemplate),
+ ),
+ });
+ }
}
diff --git a/backend/src/api/documents/dto/get-many-document-templates.dto.ts b/backend/src/api/documents/dto/get-many-document-templates.dto.ts
new file mode 100644
index 000000000..52fe19b82
--- /dev/null
+++ b/backend/src/api/documents/dto/get-many-document-templates.dto.ts
@@ -0,0 +1,3 @@
+import { BasePaginationFilterDto } from 'src/infrastructure/base/base-pagination-filter.dto';
+
+export class GetManyDocumentTemplatesDto extends BasePaginationFilterDto {}
diff --git a/backend/src/api/documents/presenters/document-template-list-view-item.presenter.ts b/backend/src/api/documents/presenters/document-template-list-view-item.presenter.ts
new file mode 100644
index 000000000..066690060
--- /dev/null
+++ b/backend/src/api/documents/presenters/document-template-list-view-item.presenter.ts
@@ -0,0 +1,64 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { Expose } from 'class-transformer';
+import { IDocumentTemplateListViewModel } from 'src/modules/documents/models/document-template-list-view.model';
+
+export class DocumentTemplateListViewItemPresenter {
+ constructor(item: IDocumentTemplateListViewModel) {
+ this.id = item.id;
+ this.name = item.name;
+ this.usageCount = item.usageCount;
+ this.lastUsage = item.lastUsage;
+ this.createdById = item.createdById;
+ this.createdByName = item.createdByName;
+ this.createdOn = item.createdOn;
+ }
+
+ @Expose()
+ @ApiProperty({
+ description: 'The unique identifier of the document template',
+ example: '525dcdf9-4117-443e-a0c3-bf652cdc5c1b',
+ })
+ id: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The name of the document template',
+ example: 'Volunteer Agreement',
+ })
+ name: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The number of times this template has been used',
+ example: 42,
+ })
+ usageCount: number;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The date when this template was last used',
+ example: '2023-05-15T14:30:00Z',
+ })
+ lastUsage: Date | null;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The ID of the user who created this template',
+ example: '8f7e9d3c-1a2b-3c4d-5e6f-7g8h9i0j1k2l',
+ })
+ createdById: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The name of the user who created this template',
+ example: 'John Doe',
+ })
+ createdByName: string;
+
+ @Expose()
+ @ApiProperty({
+ description: 'The date when this template was created',
+ example: '2023-01-01T10:00:00Z',
+ })
+ createdOn: Date;
+}
diff --git a/backend/src/migrations/1726046690797-DDLDocumentTemplateListView.ts b/backend/src/migrations/1726046690797-DDLDocumentTemplateListView.ts
new file mode 100644
index 000000000..85ed12f3f
--- /dev/null
+++ b/backend/src/migrations/1726046690797-DDLDocumentTemplateListView.ts
@@ -0,0 +1,46 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class DDLDocumentTemplateListView1726046690797
+ implements MigrationInterface
+{
+ name = 'DDLDocumentTemplateListView1726046690797';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE VIEW "DocumentTemplateListView" AS
+ SELECT
+ document_template.id,
+ document_template."name",
+ document_template.created_on,
+ document_template.organization_id,
+ "user"."id" as created_by_id,
+ "user"."name" as created_by_name,
+ count(document_contract.id) as usage_count,
+ max(document_contract.created_on) as last_usage
+ FROM
+ document_template
+ LEFT JOIN document_contract ON document_template.id = document_contract.document_template_id
+ LEFT JOIN "user" on document_template.created_by_admin_id = "user".id
+ GROUP BY
+ document_template.id,
+ "user"."name",
+ "user"."id"
+ `);
+ await queryRunner.query(
+ `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
+ [
+ 'public',
+ 'VIEW',
+ 'DocumentTemplateListView',
+ 'SELECT\n document_template.id,\n document_template."name",\n document_template.created_on,\n document_template.organization_id,\n "user"."id" as created_by_id,\n "user"."name" as created_by_name,\n count(document_contract.id) as usage_count,\n max(document_contract.created_on) as last_usage\n FROM\n document_template\n LEFT JOIN document_contract ON document_template.id = document_contract.document_template_id\n LEFT JOIN "user" on document_template.created_by_admin_id = "user".id\n GROUP BY\n document_template.id,\n "user"."name",\n "user"."id"',
+ ],
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
+ ['VIEW', 'DocumentTemplateListView', 'public'],
+ );
+ await queryRunner.query(`DROP VIEW "DocumentTemplateListView"`);
+ }
+}
diff --git a/backend/src/modules/documents/documents.module.ts b/backend/src/modules/documents/documents.module.ts
index e170d753d..993251a86 100644
--- a/backend/src/modules/documents/documents.module.ts
+++ b/backend/src/modules/documents/documents.module.ts
@@ -17,11 +17,14 @@ import { DocumentContractFacade } from './services/document-contract.facade';
import { DocumentContractListViewEntity } from './entities/document-contract-list-view.entity';
import { DocumentContractListViewRepository } from './repositories/document-contract-list-view.repository';
import { DocumentSignatureEntity } from './entities/document-signature.entity';
+import { DocumentTemplateListViewEntity } from './entities/document-template-list-view.entity';
+import { DocumentTemplateListViewRepository } from './repositories/document-template-list-view.repository';
@Module({
imports: [
TypeOrmModule.forFeature([
DocumentContractListViewEntity,
+ DocumentTemplateListViewEntity,
TemplateEntity,
ContractEntity,
DocumentTemplateEntity,
@@ -37,6 +40,7 @@ import { DocumentSignatureEntity } from './entities/document-signature.entity';
DocumentContractRepositoryService,
SignatureRepositoryService,
DocumentContractListViewRepository,
+ DocumentTemplateListViewRepository,
// Facades
TemplateFacade,
ContractFacade,
diff --git a/backend/src/modules/documents/entities/document-template-list-view.entity.ts b/backend/src/modules/documents/entities/document-template-list-view.entity.ts
new file mode 100644
index 000000000..6319769bf
--- /dev/null
+++ b/backend/src/modules/documents/entities/document-template-list-view.entity.ts
@@ -0,0 +1,48 @@
+import { ViewColumn, ViewEntity } from 'typeorm';
+
+@ViewEntity('DocumentTemplateListView', {
+ expression: `
+ SELECT
+ document_template.id,
+ document_template."name",
+ document_template.created_on,
+ document_template.organization_id,
+ "user"."id" as created_by_id,
+ "user"."name" as created_by_name,
+ count(document_contract.id) as usage_count,
+ max(document_contract.created_on) as last_usage
+ FROM
+ document_template
+ LEFT JOIN document_contract ON document_template.id = document_contract.document_template_id
+ LEFT JOIN "user" on document_template.created_by_admin_id = "user".id
+ GROUP BY
+ document_template.id,
+ "user"."name",
+ "user"."id"
+ `,
+})
+export class DocumentTemplateListViewEntity {
+ @ViewColumn({ name: 'id' })
+ id: string;
+
+ @ViewColumn({ name: 'name' })
+ name: string;
+
+ @ViewColumn({ name: 'created_on' })
+ createdOn: Date;
+
+ @ViewColumn({ name: 'created_by_id' })
+ createdById: string;
+
+ @ViewColumn({ name: 'created_by_name' })
+ createdByName: string;
+
+ @ViewColumn({ name: 'usage_count' })
+ usageCount: number;
+
+ @ViewColumn({ name: 'last_usage' })
+ lastUsage: Date;
+
+ @ViewColumn({ name: 'organization_id' })
+ organizationId: string;
+}
diff --git a/backend/src/modules/documents/models/document-template-list-view.model.ts b/backend/src/modules/documents/models/document-template-list-view.model.ts
new file mode 100644
index 000000000..79e50ef91
--- /dev/null
+++ b/backend/src/modules/documents/models/document-template-list-view.model.ts
@@ -0,0 +1,30 @@
+import { IBasePaginationFilterModel } from 'src/infrastructure/base/base-pagination-filter.model';
+import { DocumentTemplateListViewEntity } from '../entities/document-template-list-view.entity';
+
+export interface IDocumentTemplateListViewModel {
+ id: string;
+ name: string;
+
+ usageCount: number;
+ lastUsage: Date | null;
+
+ createdById: string;
+ createdByName: string;
+
+ createdOn: Date;
+}
+
+export type FindManyDocumentTemplateListViewOptions =
+ IBasePaginationFilterModel & {
+ organizationId: string;
+ };
+
+export class DocumentTemplateListViewTransformer {
+ static fromEntity(
+ entity: DocumentTemplateListViewEntity,
+ ): IDocumentTemplateListViewModel {
+ return {
+ ...entity,
+ };
+ }
+}
diff --git a/backend/src/modules/documents/repositories/document-template-list-view.repository.ts b/backend/src/modules/documents/repositories/document-template-list-view.repository.ts
new file mode 100644
index 000000000..bc657ec80
--- /dev/null
+++ b/backend/src/modules/documents/repositories/document-template-list-view.repository.ts
@@ -0,0 +1,64 @@
+import { Injectable } from '@nestjs/common';
+import { DocumentTemplateListViewEntity } from '../entities/document-template-list-view.entity';
+import {
+ Pagination,
+ RepositoryWithPagination,
+} from 'src/infrastructure/base/repository-with-pagination.class';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import {
+ DocumentTemplateListViewTransformer,
+ FindManyDocumentTemplateListViewOptions,
+ IDocumentTemplateListViewModel,
+} from '../models/document-template-list-view.model';
+import { OrderDirection } from 'src/common/enums/order-direction.enum';
+
+@Injectable()
+export class DocumentTemplateListViewRepository extends RepositoryWithPagination {
+ constructor(
+ @InjectRepository(DocumentTemplateListViewEntity)
+ private readonly documentTemplateListViewRepository: Repository,
+ ) {
+ super(documentTemplateListViewRepository);
+ }
+
+ async findMany(
+ findOptions: FindManyDocumentTemplateListViewOptions,
+ ): Promise> {
+ const {
+ orderBy,
+ orderDirection,
+ search,
+ limit,
+ page,
+
+ organizationId,
+ } = findOptions;
+
+ const query = this.documentTemplateListViewRepository
+ .createQueryBuilder('documentTemplateListView')
+ .where('documentTemplateListView.organizationId = :organizationId', {
+ organizationId,
+ })
+ .orderBy(
+ this.buildOrderByQuery(
+ orderBy || 'createdOn',
+ 'documentTemplateListView',
+ ),
+ orderDirection || OrderDirection.ASC,
+ );
+
+ if (search) {
+ query.andWhere(
+ this.buildBracketSearchQuery(['documentContractListView.name'], search),
+ );
+ }
+
+ return this.paginateQuery(
+ query,
+ limit,
+ page,
+ DocumentTemplateListViewTransformer.fromEntity,
+ );
+ }
+}
diff --git a/backend/src/modules/documents/services/document-template.facade.ts b/backend/src/modules/documents/services/document-template.facade.ts
index 9e6ed2a49..839fc8832 100644
--- a/backend/src/modules/documents/services/document-template.facade.ts
+++ b/backend/src/modules/documents/services/document-template.facade.ts
@@ -5,11 +5,18 @@ import {
FindOneDocumentTemplateOptions,
IDocumentTemplateModel,
} from '../models/document-template.model';
+import { DocumentTemplateListViewRepository } from '../repositories/document-template-list-view.repository';
+import {
+ FindManyDocumentTemplateListViewOptions,
+ IDocumentTemplateListViewModel,
+} from '../models/document-template-list-view.model';
+import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
@Injectable()
export class DocumentTemplateFacade {
constructor(
private readonly documentTemplateRepository: DocumentTemplateRepositoryService,
+ private readonly documentTemplateListViewRepository: DocumentTemplateListViewRepository,
) {}
async create(
@@ -23,4 +30,10 @@ export class DocumentTemplateFacade {
): Promise {
return this.documentTemplateRepository.findOne(findOptions);
}
+
+ async findMany(
+ findOptions: FindManyDocumentTemplateListViewOptions,
+ ): Promise> {
+ return this.documentTemplateListViewRepository.findMany(findOptions);
+ }
}
diff --git a/backend/src/usecases/documents/new_contracts/get-many-document-templates.usecase.ts b/backend/src/usecases/documents/new_contracts/get-many-document-templates.usecase.ts
new file mode 100644
index 000000000..4fc6a5cf8
--- /dev/null
+++ b/backend/src/usecases/documents/new_contracts/get-many-document-templates.usecase.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@nestjs/common';
+import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface';
+import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
+import {
+ FindManyDocumentTemplateListViewOptions,
+ IDocumentTemplateListViewModel,
+} from 'src/modules/documents/models/document-template-list-view.model';
+import { DocumentTemplateFacade } from 'src/modules/documents/services/document-template.facade';
+
+@Injectable()
+export class GetManyDocumentTemplatesUsecase
+ implements IUseCaseService>
+{
+ constructor(
+ private readonly documentTemplateFacade: DocumentTemplateFacade,
+ ) {}
+
+ public async execute(
+ findOptions: FindManyDocumentTemplateListViewOptions,
+ ): Promise> {
+ return this.documentTemplateFacade.findMany(findOptions);
+ }
+}
diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts
index b80f3b21b..8a6aef486 100644
--- a/backend/src/usecases/use-case.module.ts
+++ b/backend/src/usecases/use-case.module.ts
@@ -142,6 +142,161 @@ import { CreateDocumentTemplateUsecase } from './documents/new_contracts/create-
import { GetOneDocumentTemplateUseCase } from './documents/new_contracts/get-one-document-template.usecase';
import { CreateDocumentContractUsecase } from './documents/new_contracts/create-document-contract.usecase';
import { GetManyDocumentContractsUsecase } from './documents/new_contracts/get-many-document-contracts.usecase';
+import { GetManyDocumentTemplatesUsecase } from './documents/new_contracts/get-many-document-templates.usecase';
+
+const providers = [
+ // Organization
+ GetOrganizationUseCaseService,
+ UpdateOrganizationDescriptionUseCaseService,
+ GetOrganizationsUseCase,
+ GetOrganizationWithEventsUseCase,
+ SwitchOrganizationUsecase,
+ LeaveOrganizationUsecase,
+ RejoinOrganizationUsecase,
+ SyncWithOngHubUseCaseService,
+ // Access Codes
+ CreateAccessCodeUseCase,
+ UpdateAccessCodeUseCase,
+ GetAccessCodeUseCase,
+ GetAllAccessCodeUseCase,
+ DeleteAccessCodeUseCase,
+ // Organization structure
+ CreateOrganizationStructureUseCase,
+ GetAllOrganizationStructureUseCase,
+ GetOneOrganizationStructureUseCase,
+ DeleteOrganizationStructureUseCase,
+ UpdateOrganizationStructureUseCase,
+ GetAllOrganizationStructureByTypeUseCase,
+ // User
+ GetUserProfileUseCaseService,
+ CreateRegularUsereUseCaseService,
+ GetOneRegularUserUseCase,
+ GetManyAdminUsersUseCase,
+ UpdateUserPersonalDataUsecase,
+ UpdateRegularUserUsecase,
+ GetOneRegularUserProfileUseCase,
+ SyncUserOrganizationsUsecase,
+ DeleteAccountRegularUserUsecase,
+ // Access Requests
+ GetManyNewAccessRequestsUseCase,
+ GetManyRejectedAccessRequestsUseCase,
+ CreateAccessRequestUseCase,
+ GetAccessRequestUseCase,
+ DeleteAccessRequestUseCase,
+ ApproveAccessRequestUseCase,
+ RejectAccessRequestUseCase,
+ GetAccessRequestsForDownloadUseCase,
+ JoinOrganizationByAccessCodeUsecase,
+ CancelAccessRequestUsecase,
+ GetRejectedAccessRequestUsecase,
+ // Location
+ GetCitiesUseCase,
+ GetCountiesUseCase,
+ GetCitiesByCountyIdUseCase,
+ // Activity Types
+ CreateActivityTypeUseCase,
+ UpdateActivityTypeUseCase,
+ ActivateActivityTypeUseCase,
+ ArchiveActivityTypeUseCase,
+ GetOneActivityTypeUseCase,
+ GetManyActivityTypeUseCase,
+ // Volunteers
+ GetOneVolunteerUsecase,
+ CreateVolunteerUseCase,
+ ArchiveVolunteerUsecase,
+ BlockVolunteerUsecase,
+ ActivateVolunteerUsecase,
+ CreateVolunteerProfileUseCase,
+ GetManyVolunteersUseCase,
+ UpdateVolunteerProfileUsecase,
+ GetVolunteersForDownloadUseCase,
+ GetVolunteerProfileUsecase,
+ GetVolunteersUserDataForNotificationsUsecase,
+ GetVolunteerOrganizationStatusUsecase,
+ // Announcement
+ GetOneAnnouncementUseCase,
+ GetManyAnnouncementUseCase,
+ CreateAnnouncementUseCase,
+ UpdateAnnouncementUseCase,
+ DeleteAnnouncementUseCase,
+ GetManyAnouncementsByUserAsUsecase,
+ // Events
+ CreateEventUseCase,
+ GetOneEventUseCase,
+ UpdateEventUseCase,
+ DeleteEventUseCase,
+ PublishEventUseCase,
+ ArchiveEventUseCase,
+ GetManyForDownloadEventUseCase,
+ GetManyEventUseCase,
+ GetMyEventsUsecase,
+ GetOneEventWithVolunteerStatusUsecase,
+ GetEventsByOrganizationUsecase,
+ // Events RSVP
+ CreateEventRSVPUseCase,
+ GetOneEventRSVPUseCase,
+ DeleteEventRSVPUseCase,
+ GetManyEventRSVPUseCase,
+ GetManyForDownloadEventRSVPUseCase,
+ // Activity Log
+ CreateActivityLogByAdmin,
+ GetOneActivityLogUsecase,
+ UpdateActivityLogUsecase,
+ ApproveActivityLogUsecase,
+ RejectActivityLogUsecase,
+ GetManyActivityLogsUsecase,
+ GetActivityLogCountersUsecase,
+ GetManyForDownloadActivityLogUseCase,
+ GetActivityLogCountUsecase,
+ CreateActivityLogByRegularUser,
+ CancelActivityLogUsecase,
+ // Actions Archive
+ GetManyActionsArchiveUseCase,
+ GetManyNewsUsecase,
+ // Dashboard
+ GetDashboardVolunteerStatusTimeseriesUsecase,
+ GetDashboardVolunteerGroupedUsecase,
+ GetDashboardVolunteersHoursUseCase,
+ GetDashboardVolunteersStatusUseCase,
+ // Push Notifications
+ RegisterDevicePushTokenUseCase,
+ UnregisterDevicePushTokenUseCase,
+ GetVolunteerMonthlyNewsStatisticsUsecase,
+ GetVicStatisticsUsecase,
+ // Templates
+ CreateTemplateUsecase,
+ GetTemplatesUsecase,
+ GetOneTemplateUseCase,
+ UpdateTemplateUsecase,
+ DeleteTemplateUseCase,
+ GetAllTemplatesUsecase,
+ GetTemplatesForDownloadUsecase,
+ // NEW Templates
+ CreateDocumentTemplateUsecase,
+ GetOneDocumentTemplateUseCase,
+ GetManyDocumentTemplatesUsecase,
+ // Contracts
+ CreateContractUsecase,
+ GetManyContractsUsecase,
+ CountPendingContractsUsecase,
+ GetOneContractUsecase,
+ SignContractByVolunteer,
+ SignAndConfirmContractUsecase,
+ SignAndConfirmContractUsecase,
+ RejectContractUsecase,
+ GetContractsForDownloadUsecase,
+ DeleteContractUsecase,
+ GetVolunteerContractHistoryUsecase,
+ GetVolunteerPendingContractsUsecase,
+ CancelContractUsecase,
+ // NEW Contracts
+ CreateDocumentContractUsecase,
+ GetManyDocumentContractsUsecase,
+ // Notifications
+ UpdateSettingsUsecase,
+ // Testing PDFs
+ GeneratePDFsUseCase,
+];
@Module({
imports: [
@@ -161,310 +316,7 @@ import { GetManyDocumentContractsUsecase } from './documents/new_contracts/get-m
NotificationsModule,
DocumentsModule,
],
- providers: [
- // Organization
- GetOrganizationUseCaseService,
- UpdateOrganizationDescriptionUseCaseService,
- GetOrganizationsUseCase,
- GetOrganizationWithEventsUseCase,
- SwitchOrganizationUsecase,
- LeaveOrganizationUsecase,
- RejoinOrganizationUsecase,
- SyncWithOngHubUseCaseService,
- // Access Codes
- CreateAccessCodeUseCase,
- UpdateAccessCodeUseCase,
- GetAccessCodeUseCase,
- GetAllAccessCodeUseCase,
- DeleteAccessCodeUseCase,
- // Organization structure
- CreateOrganizationStructureUseCase,
- GetAllOrganizationStructureUseCase,
- GetOneOrganizationStructureUseCase,
- DeleteOrganizationStructureUseCase,
- UpdateOrganizationStructureUseCase,
- GetAllOrganizationStructureByTypeUseCase,
- // User
- GetUserProfileUseCaseService,
- CreateRegularUsereUseCaseService,
- GetOneRegularUserUseCase,
- GetManyAdminUsersUseCase,
- UpdateUserPersonalDataUsecase,
- UpdateRegularUserUsecase,
- GetOneRegularUserProfileUseCase,
- SyncUserOrganizationsUsecase,
- DeleteAccountRegularUserUsecase,
- // Access Requests
- GetManyNewAccessRequestsUseCase,
- GetManyRejectedAccessRequestsUseCase,
- CreateAccessRequestUseCase,
- GetAccessRequestUseCase,
- DeleteAccessRequestUseCase,
- ApproveAccessRequestUseCase,
- RejectAccessRequestUseCase,
- GetAccessRequestsForDownloadUseCase,
- JoinOrganizationByAccessCodeUsecase,
- CancelAccessRequestUsecase,
- GetRejectedAccessRequestUsecase,
- // Location
- GetCitiesUseCase,
- GetCountiesUseCase,
- GetCitiesByCountyIdUseCase,
- // Activity Types
- CreateActivityTypeUseCase,
- UpdateActivityTypeUseCase,
- ActivateActivityTypeUseCase,
- ArchiveActivityTypeUseCase,
- GetOneActivityTypeUseCase,
- GetManyActivityTypeUseCase,
- // Volunteers
- GetOneVolunteerUsecase,
- CreateVolunteerUseCase,
- ArchiveVolunteerUsecase,
- BlockVolunteerUsecase,
- ActivateVolunteerUsecase,
- CreateVolunteerProfileUseCase,
- GetManyVolunteersUseCase,
- UpdateVolunteerProfileUsecase,
- GetVolunteersForDownloadUseCase,
- GetVolunteerProfileUsecase,
- GetVolunteersUserDataForNotificationsUsecase,
- GetVolunteerOrganizationStatusUsecase,
- // Announcement
- GetOneAnnouncementUseCase,
- GetManyAnnouncementUseCase,
- CreateAnnouncementUseCase,
- UpdateAnnouncementUseCase,
- DeleteAnnouncementUseCase,
- GetManyAnouncementsByUserAsUsecase,
- // Events
- CreateEventUseCase,
- GetOneEventUseCase,
- UpdateEventUseCase,
- DeleteEventUseCase,
- PublishEventUseCase,
- ArchiveEventUseCase,
- GetManyForDownloadEventUseCase,
- GetManyEventUseCase,
- GetMyEventsUsecase,
- GetOneEventWithVolunteerStatusUsecase,
- GetEventsByOrganizationUsecase,
- // Events RSVP
- CreateEventRSVPUseCase,
- GetOneEventRSVPUseCase,
- DeleteEventRSVPUseCase,
- GetManyEventRSVPUseCase,
- GetManyForDownloadEventRSVPUseCase,
- // Activity Log
- CreateActivityLogByAdmin,
- GetOneActivityLogUsecase,
- UpdateActivityLogUsecase,
- ApproveActivityLogUsecase,
- RejectActivityLogUsecase,
- GetManyActivityLogsUsecase,
- GetActivityLogCountersUsecase,
- GetManyForDownloadActivityLogUseCase,
- GetActivityLogCountUsecase,
- CreateActivityLogByRegularUser,
- CancelActivityLogUsecase,
- // Actions Archive
- GetManyActionsArchiveUseCase,
- GetManyNewsUsecase,
- // Dashboard
- GetDashboardVolunteerStatusTimeseriesUsecase,
- GetDashboardVolunteerGroupedUsecase,
- GetDashboardVolunteersHoursUseCase,
- GetDashboardVolunteersStatusUseCase,
- // Push Notifications
- RegisterDevicePushTokenUseCase,
- UnregisterDevicePushTokenUseCase,
- GetVolunteerMonthlyNewsStatisticsUsecase,
- GetVicStatisticsUsecase,
- // Templates
- CreateTemplateUsecase,
- GetTemplatesUsecase,
- GetOneTemplateUseCase,
- UpdateTemplateUsecase,
- DeleteTemplateUseCase,
- GetAllTemplatesUsecase,
- GetTemplatesForDownloadUsecase,
- // NEW Templates
- CreateDocumentTemplateUsecase,
- GetOneDocumentTemplateUseCase,
- // Contracts
- CreateContractUsecase,
- GetManyContractsUsecase,
- CountPendingContractsUsecase,
- GetOneContractUsecase,
- SignContractByVolunteer,
- SignAndConfirmContractUsecase,
- SignAndConfirmContractUsecase,
- RejectContractUsecase,
- GetContractsForDownloadUsecase,
- DeleteContractUsecase,
- GetVolunteerContractHistoryUsecase,
- GetVolunteerPendingContractsUsecase,
- CancelContractUsecase,
- // NEW Contracts
- CreateDocumentContractUsecase,
- GetManyDocumentContractsUsecase,
- // Notifications
- UpdateSettingsUsecase,
- // Testing PDFs
- GeneratePDFsUseCase,
- ],
- exports: [
- // Organization
- GetOrganizationUseCaseService,
- UpdateOrganizationDescriptionUseCaseService,
- GetOrganizationsUseCase,
- GetOrganizationWithEventsUseCase,
- SwitchOrganizationUsecase,
- LeaveOrganizationUsecase,
- RejoinOrganizationUsecase,
- SyncWithOngHubUseCaseService,
- // Access Codes
- CreateAccessCodeUseCase,
- UpdateAccessCodeUseCase,
- GetAccessCodeUseCase,
- GetAllAccessCodeUseCase,
- DeleteAccessCodeUseCase,
- // Organization Structure
- CreateOrganizationStructureUseCase,
- GetAllOrganizationStructureUseCase,
- GetOneOrganizationStructureUseCase,
- DeleteOrganizationStructureUseCase,
- UpdateOrganizationStructureUseCase,
- GetAllOrganizationStructureByTypeUseCase,
- // User
- GetUserProfileUseCaseService,
- CreateRegularUsereUseCaseService,
- GetOneRegularUserUseCase,
- GetManyAdminUsersUseCase,
- UpdateUserPersonalDataUsecase,
- UpdateRegularUserUsecase,
- GetOneRegularUserProfileUseCase,
- SyncUserOrganizationsUsecase,
- DeleteAccountRegularUserUsecase,
- // Access Requests
- GetManyNewAccessRequestsUseCase,
- GetManyRejectedAccessRequestsUseCase,
- CreateAccessRequestUseCase,
- GetAccessRequestUseCase,
- DeleteAccessRequestUseCase,
- ApproveAccessRequestUseCase,
- RejectAccessRequestUseCase,
- GetAccessRequestsForDownloadUseCase,
- JoinOrganizationByAccessCodeUsecase,
- CancelAccessRequestUsecase,
- GetRejectedAccessRequestUsecase,
- // Location
- GetCitiesUseCase,
- GetCountiesUseCase,
- GetCitiesByCountyIdUseCase,
- // Activity Types
- CreateActivityTypeUseCase,
- UpdateActivityTypeUseCase,
- ActivateActivityTypeUseCase,
- ArchiveActivityTypeUseCase,
- GetOneActivityTypeUseCase,
- GetManyActivityTypeUseCase,
- // Volunteers
- GetOneVolunteerUsecase,
- CreateVolunteerUseCase,
- ArchiveVolunteerUsecase,
- BlockVolunteerUsecase,
- ActivateVolunteerUsecase,
- CreateVolunteerProfileUseCase,
- GetManyVolunteersUseCase,
- UpdateVolunteerProfileUsecase,
- GetVolunteersForDownloadUseCase,
- GetVolunteerProfileUsecase,
- GetVolunteersUserDataForNotificationsUsecase,
- GetVolunteerOrganizationStatusUsecase,
- // Announcement
- GetOneAnnouncementUseCase,
- GetManyAnnouncementUseCase,
- CreateAnnouncementUseCase,
- UpdateAnnouncementUseCase,
- DeleteAnnouncementUseCase,
- CreateEventUseCase,
- GetManyAnouncementsByUserAsUsecase,
- // Events
- CreateEventUseCase,
- GetOneEventUseCase,
- UpdateEventUseCase,
- DeleteEventUseCase,
- PublishEventUseCase,
- ArchiveEventUseCase,
- GetManyEventUseCase,
- GetManyForDownloadEventUseCase,
- GetMyEventsUsecase,
- GetOneEventWithVolunteerStatusUsecase,
- GetEventsByOrganizationUsecase,
- // Events RSVP
- CreateEventRSVPUseCase,
- GetOneEventRSVPUseCase,
- DeleteEventRSVPUseCase,
- GetManyEventRSVPUseCase,
- GetManyForDownloadEventRSVPUseCase,
- // Activity Log
- CreateActivityLogByAdmin,
- GetOneActivityLogUsecase,
- UpdateActivityLogUsecase,
- ApproveActivityLogUsecase,
- RejectActivityLogUsecase,
- GetManyActivityLogsUsecase,
- GetActivityLogCountersUsecase,
- GetManyForDownloadActivityLogUseCase,
- GetActivityLogCountUsecase,
- CreateActivityLogByRegularUser,
- CancelActivityLogUsecase,
- // Actions Archive
- GetManyActionsArchiveUseCase,
- GetManyNewsUsecase,
- // Dashboard
- GetDashboardVolunteerStatusTimeseriesUsecase,
- GetDashboardVolunteerGroupedUsecase,
- GetDashboardVolunteersHoursUseCase,
- GetDashboardVolunteersStatusUseCase,
- // Push Notifications
- RegisterDevicePushTokenUseCase,
- UnregisterDevicePushTokenUseCase,
- GetVolunteerMonthlyNewsStatisticsUsecase,
- // Templates
- CreateTemplateUsecase,
- GetTemplatesUsecase,
- GetOneTemplateUseCase,
- UpdateTemplateUsecase,
- DeleteTemplateUseCase,
- GetAllTemplatesUsecase,
- GetTemplatesForDownloadUsecase,
- // NEW Templates
- CreateDocumentTemplateUsecase,
- GetOneDocumentTemplateUseCase,
- // Contracts
- CreateContractUsecase,
- GetManyContractsUsecase,
- CountPendingContractsUsecase,
- GetOneContractUsecase,
- SignContractByVolunteer,
- SignAndConfirmContractUsecase,
- SignAndConfirmContractUsecase,
- RejectContractUsecase,
- GetContractsForDownloadUsecase,
- DeleteContractUsecase,
- GetVolunteerContractHistoryUsecase,
- GetVolunteerPendingContractsUsecase,
- CancelContractUsecase,
- // NEW Contracts
- CreateDocumentContractUsecase,
- GetManyDocumentContractsUsecase,
- // Notifications
- UpdateSettingsUsecase,
- GetVicStatisticsUsecase,
- // Testing PDFs
- GeneratePDFsUseCase,
- ],
+ providers: providers,
+ exports: providers,
})
export class UseCaseModule {}
From b17c3d289d9f132ee8f326fdc148f504570629b5 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Wed, 11 Sep 2024 13:44:45 +0300
Subject: [PATCH 28/55] feat: [Contracts] Fix swagger missing limit and page
from base pagination filters
---
.../src/api/documents/document-contract.controller.ts | 6 +++++-
.../src/api/documents/document-template.controller.ts | 6 +++++-
.../infrastructure/base/base-pagination-filter.dto.ts | 10 ++++++++--
3 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/backend/src/api/documents/document-contract.controller.ts b/backend/src/api/documents/document-contract.controller.ts
index 612d96df2..cffdad15e 100644
--- a/backend/src/api/documents/document-contract.controller.ts
+++ b/backend/src/api/documents/document-contract.controller.ts
@@ -8,7 +8,10 @@ import { WebJwtAuthGuard } from 'src/modules/auth/guards/jwt-web.guard';
import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
import { GetManyDocumentContractsUsecase } from 'src/usecases/documents/new_contracts/get-many-document-contracts.usecase';
import { DocumentContractListViewItemPresenter } from './presenters/document-contract-list-view-item.presenter';
-import { PaginatedPresenter } from 'src/infrastructure/presenters/generic-paginated.presenter';
+import {
+ ApiPaginatedResponse,
+ PaginatedPresenter,
+} from 'src/infrastructure/presenters/generic-paginated.presenter';
import { GetManyDocumentContractsDto } from './dto/get-many-document-contracts.dto';
@ApiBearerAuth()
@@ -35,6 +38,7 @@ export class DocumentContractController {
}
@Get('')
+ @ApiPaginatedResponse(DocumentContractListViewItemPresenter)
async getDocumentContracts(
@Query() filters: GetManyDocumentContractsDto,
@ExtractUser() { organizationId }: IAdminUserModel,
diff --git a/backend/src/api/documents/document-template.controller.ts b/backend/src/api/documents/document-template.controller.ts
index ef171ff61..41a994174 100644
--- a/backend/src/api/documents/document-template.controller.ts
+++ b/backend/src/api/documents/document-template.controller.ts
@@ -19,7 +19,10 @@ import { GetOneDocumentTemplateUseCase } from 'src/usecases/documents/new_contra
import { GetManyDocumentTemplatesDto } from './dto/get-many-document-templates.dto';
import { DocumentTemplateListViewItemPresenter } from './presenters/document-template-list-view-item.presenter';
import { GetManyDocumentTemplatesUsecase } from 'src/usecases/documents/new_contracts/get-many-document-templates.usecase';
-import { PaginatedPresenter } from 'src/infrastructure/presenters/generic-paginated.presenter';
+import {
+ ApiPaginatedResponse,
+ PaginatedPresenter,
+} from 'src/infrastructure/presenters/generic-paginated.presenter';
@ApiBearerAuth()
@UseGuards(WebJwtAuthGuard)
@@ -61,6 +64,7 @@ export class DocumentTemplateController {
}
@Get()
+ @ApiPaginatedResponse(DocumentTemplateListViewItemPresenter)
async getMany(
@Query() query: GetManyDocumentTemplatesDto,
@ExtractUser() { organizationId }: IAdminUserModel,
diff --git a/backend/src/infrastructure/base/base-pagination-filter.dto.ts b/backend/src/infrastructure/base/base-pagination-filter.dto.ts
index 0c635d0c5..fc28f4dd0 100644
--- a/backend/src/infrastructure/base/base-pagination-filter.dto.ts
+++ b/backend/src/infrastructure/base/base-pagination-filter.dto.ts
@@ -1,24 +1,30 @@
import { Type } from 'class-transformer';
import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator';
import { OrderDirection } from 'src/common/enums/order-direction.enum';
+import { ApiProperty } from '@nestjs/swagger';
export class BasePaginationFilterDto {
+ @ApiProperty({ default: 10 })
@IsNumber()
@Type(() => Number)
- limit = 10;
+ limit: number = 10;
+ @ApiProperty({ default: 1 })
@IsNumber()
@Type(() => Number)
- page = 1;
+ page: number = 1;
+ @ApiProperty({ required: false })
@IsString()
@IsOptional()
search?: string;
+ @ApiProperty({ required: false })
@IsOptional()
@IsString()
orderBy?: string;
+ @ApiProperty({ enum: OrderDirection, required: false })
@IsOptional()
@IsEnum(OrderDirection)
orderDirection?: OrderDirection;
From 566835d077015f26d0256c2d22f15f0149048655 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Wed, 11 Sep 2024 16:10:44 +0300
Subject: [PATCH 29/55] feat: [Contracts] Add volunteer signature usecase and
endpoint
---
.../documents-contract.controller.ts | 37 ++++
.../documents/dto/SignDocumentContract.dto.ts | 19 ++
backend/src/api/api.module.ts | 2 +
.../1726053758155-RenameTutorToGuardian.ts | 29 +++
.../src/modules/documents/documents.module.ts | 7 +-
.../entities/document-contract.entity.ts | 8 +-
.../models/document-contract.model.ts | 11 +-
.../document-contract.repository.ts | 4 +
.../document-signature.repository.ts | 8 +-
.../services/document-contract.facade.ts | 4 +
.../services/document-signature.facade.ts | 14 ++
...-document-contract-by-volunteer.usecase.ts | 170 ++++++++++++++++++
backend/src/usecases/use-case.module.ts | 2 +
13 files changed, 301 insertions(+), 14 deletions(-)
create mode 100644 backend/src/api/_mobile/documents/documents-contract.controller.ts
create mode 100644 backend/src/api/_mobile/documents/dto/SignDocumentContract.dto.ts
create mode 100644 backend/src/migrations/1726053758155-RenameTutorToGuardian.ts
create mode 100644 backend/src/modules/documents/services/document-signature.facade.ts
create mode 100644 backend/src/usecases/documents/new_contracts/sign-document-contract-by-volunteer.usecase.ts
diff --git a/backend/src/api/_mobile/documents/documents-contract.controller.ts b/backend/src/api/_mobile/documents/documents-contract.controller.ts
new file mode 100644
index 000000000..faf38fa3d
--- /dev/null
+++ b/backend/src/api/_mobile/documents/documents-contract.controller.ts
@@ -0,0 +1,37 @@
+import { Body, Param, Patch, UseGuards } from '@nestjs/common';
+import { Controller } from '@nestjs/common';
+import { ApiBearerAuth, ApiParam } from '@nestjs/swagger';
+import { MobileJwtAuthGuard } from 'src/modules/auth/guards/jwt-mobile.guard';
+import { ExtractUser } from 'src/common/decorators/extract-user.decorator';
+import { IRegularUserModel } from 'src/modules/user/models/regular-user.model';
+import { UuidValidationPipe } from 'src/infrastructure/pipes/uuid.pipe';
+import { SignDocumentContractByVolunteerUsecase } from 'src/usecases/documents/new_contracts/sign-document-contract-by-volunteer.usecase';
+import { SignDocumentContractDto } from './dto/SignDocumentContract.dto';
+
+// @UseGuards(MobileJwtAuthGuard, ContractVolunteerGuard)
+@UseGuards(MobileJwtAuthGuard)
+@ApiBearerAuth()
+@Controller('mobile/documents/contracts')
+export class MobileDocumentsContractController {
+ constructor(
+ private readonly signDocumentContractByVolunteerUsecase: SignDocumentContractByVolunteerUsecase,
+ ) {}
+
+ @ApiParam({ name: 'contractId', type: 'string' })
+ @Patch(':contractId/sign')
+ async sign(
+ @Body() body: SignDocumentContractDto,
+ @ExtractUser() { id }: IRegularUserModel,
+ @Param('contractId', UuidValidationPipe) contractId: string,
+ ): Promise {
+ const contract = await this.signDocumentContractByVolunteerUsecase.execute({
+ contractId,
+ userId: id,
+ organizationId: body.organizationId, // TODO: can use activeOrganization but is a bit danger if the mobile doesn't set it
+ volunteerSignatureBase64: body.volunteerSignatureBase64,
+ legalGuardianSignatureBase64: body.legalGuardianSignatureBase64,
+ });
+
+ return contract;
+ }
+}
diff --git a/backend/src/api/_mobile/documents/dto/SignDocumentContract.dto.ts b/backend/src/api/_mobile/documents/dto/SignDocumentContract.dto.ts
new file mode 100644
index 000000000..9a513d964
--- /dev/null
+++ b/backend/src/api/_mobile/documents/dto/SignDocumentContract.dto.ts
@@ -0,0 +1,19 @@
+import { IsString, IsOptional, IsUUID } from 'class-validator';
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
+
+export class SignDocumentContractDto {
+ @ApiProperty({ description: 'The ID of the organization' })
+ @IsUUID()
+ organizationId: string;
+
+ @ApiProperty({ description: 'Base64 encoded volunteer signature' })
+ @IsString()
+ volunteerSignatureBase64: string;
+
+ @ApiPropertyOptional({
+ description: 'Base64 encoded legal guardian signature',
+ })
+ @IsOptional()
+ @IsString()
+ legalGuardianSignatureBase64?: string;
+}
diff --git a/backend/src/api/api.module.ts b/backend/src/api/api.module.ts
index 26ed48ce6..bcce72497 100644
--- a/backend/src/api/api.module.ts
+++ b/backend/src/api/api.module.ts
@@ -33,6 +33,7 @@ import { MobileSettingsController } from './_mobile/settings/settings-controller
import { MobileNewsController } from './_mobile/news/news.controller';
import { DocumentTemplateController } from './documents/document-template.controller';
import { DocumentContractController } from './documents/document-contract.controller';
+import { MobileDocumentsContractController } from './_mobile/documents/documents-contract.controller';
@Module({
imports: [UseCaseModule],
@@ -71,6 +72,7 @@ import { DocumentContractController } from './documents/document-contract.contro
MobileAnouncementsController,
MobileSettingsController,
MobileNewsController,
+ MobileDocumentsContractController,
],
})
export class ApiModule {}
diff --git a/backend/src/migrations/1726053758155-RenameTutorToGuardian.ts b/backend/src/migrations/1726053758155-RenameTutorToGuardian.ts
new file mode 100644
index 000000000..821c7f803
--- /dev/null
+++ b/backend/src/migrations/1726053758155-RenameTutorToGuardian.ts
@@ -0,0 +1,29 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class RenameTutorToGuardian1726053758155 implements MigrationInterface {
+ name = 'RenameTutorToGuardian1726053758155';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP CONSTRAINT "FK_8586b2b6c023b4a93d301363004"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" RENAME COLUMN "tutor_signature_id" TO "legal_guardian_signature_id"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD CONSTRAINT "FK_219866394581cf64cb2b7194b88" FOREIGN KEY ("legal_guardian_signature_id") REFERENCES "document_signature"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" DROP CONSTRAINT "FK_219866394581cf64cb2b7194b88"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" RENAME COLUMN "legal_guardian_signature_id" TO "tutor_signature_id"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "document_contract" ADD CONSTRAINT "FK_8586b2b6c023b4a93d301363004" FOREIGN KEY ("tutor_signature_id") REFERENCES "document_signature"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ }
+}
diff --git a/backend/src/modules/documents/documents.module.ts b/backend/src/modules/documents/documents.module.ts
index 993251a86..d9ca336aa 100644
--- a/backend/src/modules/documents/documents.module.ts
+++ b/backend/src/modules/documents/documents.module.ts
@@ -12,13 +12,14 @@ import { DocumentTemplateFacade } from './services/document-template.facade';
import { DocumentTemplateEntity } from './entities/document-template.entity';
import { DocumentContractEntity } from './entities/document-contract.entity';
import { DocumentContractRepositoryService } from './repositories/document-contract.repository';
-import { SignatureRepositoryService } from './repositories/document-signature.repository';
import { DocumentContractFacade } from './services/document-contract.facade';
import { DocumentContractListViewEntity } from './entities/document-contract-list-view.entity';
import { DocumentContractListViewRepository } from './repositories/document-contract-list-view.repository';
import { DocumentSignatureEntity } from './entities/document-signature.entity';
import { DocumentTemplateListViewEntity } from './entities/document-template-list-view.entity';
import { DocumentTemplateListViewRepository } from './repositories/document-template-list-view.repository';
+import { DocumentSignatureRepository } from './repositories/document-signature.repository';
+import { DocumentSignatureFacade } from './services/document-signature.facade';
@Module({
imports: [
@@ -38,7 +39,7 @@ import { DocumentTemplateListViewRepository } from './repositories/document-temp
ContractRepositoryService,
DocumentTemplateRepositoryService,
DocumentContractRepositoryService,
- SignatureRepositoryService,
+ DocumentSignatureRepository,
DocumentContractListViewRepository,
DocumentTemplateListViewRepository,
// Facades
@@ -46,6 +47,7 @@ import { DocumentTemplateListViewRepository } from './repositories/document-temp
ContractFacade,
DocumentTemplateFacade,
DocumentContractFacade,
+ DocumentSignatureFacade,
// Services
PDFGenerator,
],
@@ -56,6 +58,7 @@ import { DocumentTemplateListViewRepository } from './repositories/document-temp
PDFGenerator,
DocumentTemplateFacade,
DocumentContractFacade,
+ DocumentSignatureFacade,
],
})
export class DocumentsModule {}
diff --git a/backend/src/modules/documents/entities/document-contract.entity.ts b/backend/src/modules/documents/entities/document-contract.entity.ts
index 8cb85feba..e80c701d5 100644
--- a/backend/src/modules/documents/entities/document-contract.entity.ts
+++ b/backend/src/modules/documents/entities/document-contract.entity.ts
@@ -118,14 +118,14 @@ export class DocumentContractEntity extends BaseEntity {
@Column({
type: 'varchar',
- name: 'tutor_signature_id',
+ name: 'legal_guardian_signature_id',
nullable: true,
})
- tutorSignatureId: string;
+ legalGuardianSignatureId: string;
@ManyToOne(() => DocumentSignatureEntity)
- @JoinColumn({ name: 'tutor_signature_id' })
- tutorSignature: DocumentSignatureEntity;
+ @JoinColumn({ name: 'legal_guardian_signature_id' })
+ legalGuardianSignature: DocumentSignatureEntity;
// // ==================== APPROVAL =================================
diff --git a/backend/src/modules/documents/models/document-contract.model.ts b/backend/src/modules/documents/models/document-contract.model.ts
index 3ccb8f894..97254f894 100644
--- a/backend/src/modules/documents/models/document-contract.model.ts
+++ b/backend/src/modules/documents/models/document-contract.model.ts
@@ -48,7 +48,7 @@ export interface IDocumentContractModel extends IBaseModel {
ngoLegalRepresentativeSignatureId?: string;
volunteerSignatureId?: string;
- tutorSignatureId?: string;
+ legalGuardianSignatureId?: string;
}
export type CreateDocumentContractOptions = {
@@ -72,10 +72,15 @@ export type UpdateDocumentContractOptions = {
filePath?: string;
ngoLegalRepresentativeSignatureId?: string;
volunteerSignatureId?: string;
- tutorSignatureId?: string;
+ legalGuardianSignatureId?: string;
};
-export type FindOneDocumentContractOptions = Pick;
+export type FindOneDocumentContractOptions = Partial<
+ Pick<
+ IDocumentContractModel,
+ 'id' | 'volunteerId' | 'organizationId' | 'status'
+ >
+>;
export class DocumentContractTransformer {
static fromEntity(entity: DocumentContractEntity): IDocumentContractModel {
diff --git a/backend/src/modules/documents/repositories/document-contract.repository.ts b/backend/src/modules/documents/repositories/document-contract.repository.ts
index 97a06cc87..2202a189b 100644
--- a/backend/src/modules/documents/repositories/document-contract.repository.ts
+++ b/backend/src/modules/documents/repositories/document-contract.repository.ts
@@ -43,6 +43,10 @@ export class DocumentContractRepositoryService extends RepositoryWithPagination<
return DocumentContractTransformer.fromEntity(documentContract);
}
+ async exists(options: FindOneDocumentContractOptions): Promise {
+ return this.documentContractRepository.exists({ where: options });
+ }
+
async update(
id: string,
updates: UpdateDocumentContractOptions,
diff --git a/backend/src/modules/documents/repositories/document-signature.repository.ts b/backend/src/modules/documents/repositories/document-signature.repository.ts
index 6fd9ca9e2..99996333e 100644
--- a/backend/src/modules/documents/repositories/document-signature.repository.ts
+++ b/backend/src/modules/documents/repositories/document-signature.repository.ts
@@ -8,17 +8,15 @@ import {
import { Injectable } from '@nestjs/common';
@Injectable()
-export class SignatureRepositoryService {
+export class DocumentSignatureRepository {
constructor(
@InjectRepository(DocumentSignatureEntity)
private readonly signatureRepository: Repository,
) {}
- async create(
- newSignature: CreateSignatureOptions,
- ): Promise {
+ async create(newSignature: CreateSignatureOptions): Promise {
const signature = await this.signatureRepository.save(newSignature);
- return signature;
+ return signature.id;
}
async findOne(
diff --git a/backend/src/modules/documents/services/document-contract.facade.ts b/backend/src/modules/documents/services/document-contract.facade.ts
index 63a46b775..63f8b9c73 100644
--- a/backend/src/modules/documents/services/document-contract.facade.ts
+++ b/backend/src/modules/documents/services/document-contract.facade.ts
@@ -32,6 +32,10 @@ export class DocumentContractFacade {
return this.documentContractRepository.findOne(options);
}
+ async exists(options: FindOneDocumentContractOptions): Promise {
+ return this.documentContractRepository.exists(options);
+ }
+
async findMany(
options: FindManyDocumentContractListViewOptions,
): Promise> {
diff --git a/backend/src/modules/documents/services/document-signature.facade.ts b/backend/src/modules/documents/services/document-signature.facade.ts
new file mode 100644
index 000000000..bd9a88214
--- /dev/null
+++ b/backend/src/modules/documents/services/document-signature.facade.ts
@@ -0,0 +1,14 @@
+import { Injectable } from '@nestjs/common';
+import { DocumentSignatureRepository } from '../repositories/document-signature.repository';
+import { CreateSignatureOptions } from '../models/document-signature.model';
+
+@Injectable()
+export class DocumentSignatureFacade {
+ constructor(
+ private readonly documentSignatureRepository: DocumentSignatureRepository,
+ ) {}
+
+ async create(newSignature: CreateSignatureOptions): Promise {
+ return this.documentSignatureRepository.create(newSignature);
+ }
+}
diff --git a/backend/src/usecases/documents/new_contracts/sign-document-contract-by-volunteer.usecase.ts b/backend/src/usecases/documents/new_contracts/sign-document-contract-by-volunteer.usecase.ts
new file mode 100644
index 000000000..77de58371
--- /dev/null
+++ b/backend/src/usecases/documents/new_contracts/sign-document-contract-by-volunteer.usecase.ts
@@ -0,0 +1,170 @@
+import { Injectable } from '@nestjs/common';
+import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface';
+import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service';
+import { DocumentContractStatus } from 'src/modules/documents/enums/contract-status.enum';
+import { ContractExceptionMessages } from 'src/modules/documents/exceptions/contract.exceptions';
+import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade';
+import { DocumentSignatureFacade } from 'src/modules/documents/services/document-signature.facade';
+import { VolunteerFacade } from 'src/modules/volunteer/services/volunteer.facade';
+
+// ┌─────────────────────────────────────────────────────────────────────────┐
+// │ Business Rules for SignDocumentContractByVolunteerUsecase: │
+// │ │
+// │ 1. Volunteer Authentication: │
+// │ - The volunteer must exist and be associated with the given │
+// │ organization. │
+// │ - If the volunteer is not found or not part of the organization, │
+// │ throw a not found exception. │
+// │ │s
+// │ 2. Contract Validation: │
+// │ - The contract must exist and be in the PENDING_VOLUNTEER_SIGNATURE │
+// │ status. │
+// │ - The contract must be assigned to the current volunteer. │
+// │ - The contract must belong to the user's organization. │
+// │ - If any of these conditions are not met, throw a not found │
+// │ exception. │
+// │ │
+// │ 3. Signature Requirements: │
+// │ - A volunteer signature (volunteerSignatureBase64) is mandatory. │
+// │ - A legal guardian signature (legalGuardianSignatureBase64) is │
+// │ optional. │
+// │ │
+// │ 4. Signature Creation: │
+// │ - Create a new signature record for the volunteer using the │
+// │ provided signature. │
+// │ - If a legal guardian signature is provided, create a separate │
+// │ signature record for it. │
+// │ │
+// │ 5. Contract Update: │
+// │ - Update the contract with the newly created signature IDs. │
+// │ - Change the contract status to PENDING_APPROVAL_NGO. │
+// │ │
+// │ 6. Error Handling: │
+// │ - Any failures in the process should throw appropriate exceptions. │
+// │ - Use the ExceptionsService to handle and throw standardized │
+// │ exceptions. │
+// │ │
+// │ 7. Transactional Integrity: │
+// │ - Ensure that all database operations (signature creation and │
+// │ contract update) are performed atomically. │
+// │ - If any part of the process fails, all changes should be rolled │
+// │ back. │
+// │ │
+// │ 8. Audit Trail: // TODO: Implement this │
+// │ - Track the event in an Actions Archive for auditing purposes. │
+// │ │
+// │ 9. Authorization: │
+// │ - Ensure that only the assigned volunteer can sign their own │
+// │ contract. │
+// │ │
+// │ 10. Data Validation: // TODO: Implement this │
+// │ - Validate the format and content of the signature data before │
+// │ processing. │
+// └─────────────────────────────────────────────────────────────────────────┘
+
+@Injectable()
+export class SignDocumentContractByVolunteerUsecase
+ implements IUseCaseService
+{
+ constructor(
+ private readonly documentContractFacade: DocumentContractFacade,
+ private readonly documentSignatureFacade: DocumentSignatureFacade,
+ private readonly volunteerFacade: VolunteerFacade,
+ private readonly exceptionService: ExceptionsService,
+ ) {}
+
+ public async execute({
+ contractId,
+ userId,
+ organizationId,
+ volunteerSignatureBase64,
+ legalGuardianSignatureBase64,
+ }: {
+ contractId: string;
+ userId: string;
+ organizationId: string;
+ volunteerSignatureBase64: string;
+ legalGuardianSignatureBase64?: string;
+ }): Promise {
+ // ┌─────────────────────────────────────────────────────────────────────┐
+ // │ Verify volunteer existence: │
+ // │ │
+ // │ 1. Volunteer must exist and be part of the organization │
+ // │ │
+ // │ This ensures that the volunteer is valid and authorized to sign. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ const volunteer = await this.volunteerFacade.find({
+ userId: userId,
+ organizationId,
+ });
+ if (!volunteer) {
+ this.exceptionService.notFoundException({
+ message: 'Volunteer is not part of the organization',
+ code_error: 'VOLUNTEER_NOT_PART_OF_ORGANIZATION',
+ });
+ }
+
+ // ┌─────────────────────────────────────────────────────────────────────┐
+ // │ Verify contract existence and eligibility: │
+ // │ │
+ // │ 1. Status must be PENDING_VOLUNTEER_SIGNATURE │
+ // │ 2. Contract is assigned to the current volunteer │
+ // │ 3. Contract belongs to the user's organization │
+ // │ │
+ // │ This ensures proper authorization and workflow compliance. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ const contractExists = await this.documentContractFacade.exists({
+ id: contractId,
+ volunteerId: volunteer.id,
+ organizationId,
+ status: DocumentContractStatus.PENDING_VOLUNTEER_SIGNATURE,
+ });
+
+ if (!contractExists) {
+ this.exceptionService.notFoundException(
+ ContractExceptionMessages.CONTRACT_002,
+ );
+ }
+
+ // ┌─────────────────────────────────────────────────────────────────────┐
+ // │ Create signatures: │
+ // │ │
+ // │ 1. Volunteer signature │
+ // │ 2. Legal guardian signature (optional) │
+ // │ │
+ // │ This ensures that both signatures are securely stored. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ // Create the volunteer signature
+ const volunteerSignatureId = await this.documentSignatureFacade.create({
+ userId: userId,
+ signature: volunteerSignatureBase64,
+ });
+
+ // Create the legal guardian signature
+ const legalGuardianSignatureId = legalGuardianSignatureBase64
+ ? await this.documentSignatureFacade.create({
+ userId: userId,
+ signature: legalGuardianSignatureBase64,
+ })
+ : null;
+
+ // ┌─────────────────────────────────────────────────────────────────────┐
+ // │ Update contract: │
+ // │ │
+ // │ 1. Update the contract with the signatures │
+ // │ 2. Set the status to PENDING_APPROVAL_NGO │
+ // │ │
+ // │ This ensures that the contract is updated with the signatures and │
+ // │ ready for further processing. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ await this.documentContractFacade.update(contractId, {
+ status: DocumentContractStatus.PENDING_APPROVAL_NGO,
+ volunteerSignatureId: volunteerSignatureId,
+ legalGuardianSignatureId: legalGuardianSignatureId,
+ });
+
+ // Track event in Actions Archive
+
+ return;
+ }
+}
diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts
index 8a6aef486..7fc95532e 100644
--- a/backend/src/usecases/use-case.module.ts
+++ b/backend/src/usecases/use-case.module.ts
@@ -143,6 +143,7 @@ import { GetOneDocumentTemplateUseCase } from './documents/new_contracts/get-one
import { CreateDocumentContractUsecase } from './documents/new_contracts/create-document-contract.usecase';
import { GetManyDocumentContractsUsecase } from './documents/new_contracts/get-many-document-contracts.usecase';
import { GetManyDocumentTemplatesUsecase } from './documents/new_contracts/get-many-document-templates.usecase';
+import { SignDocumentContractByVolunteerUsecase } from './documents/new_contracts/sign-document-contract-by-volunteer.usecase';
const providers = [
// Organization
@@ -292,6 +293,7 @@ const providers = [
// NEW Contracts
CreateDocumentContractUsecase,
GetManyDocumentContractsUsecase,
+ SignDocumentContractByVolunteerUsecase,
// Notifications
UpdateSettingsUsecase,
// Testing PDFs
From 7077b55d7de223fc89d98068774e6157536c2cf1 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Wed, 11 Sep 2024 16:27:23 +0300
Subject: [PATCH 30/55] feat: [Contracts] Endpoint for Reject Document Contract
by volunteer
---
.../documents-contract.controller.ts | 22 ++-
.../RejectDocumentContractByVolunteer.dto.ts | 13 ++
...=> SignDocumentContractByVolunteer.dto.ts} | 2 +-
...t-document-contact-by-volunteer.usecase.ts | 145 ++++++++++++++++++
backend/src/usecases/use-case.module.ts | 2 +
5 files changed, 181 insertions(+), 3 deletions(-)
create mode 100644 backend/src/api/_mobile/documents/dto/RejectDocumentContractByVolunteer.dto.ts
rename backend/src/api/_mobile/documents/dto/{SignDocumentContract.dto.ts => SignDocumentContractByVolunteer.dto.ts} (91%)
create mode 100644 backend/src/usecases/documents/new_contracts/reject-document-contact-by-volunteer.usecase.ts
diff --git a/backend/src/api/_mobile/documents/documents-contract.controller.ts b/backend/src/api/_mobile/documents/documents-contract.controller.ts
index faf38fa3d..5026ed709 100644
--- a/backend/src/api/_mobile/documents/documents-contract.controller.ts
+++ b/backend/src/api/_mobile/documents/documents-contract.controller.ts
@@ -6,7 +6,9 @@ import { ExtractUser } from 'src/common/decorators/extract-user.decorator';
import { IRegularUserModel } from 'src/modules/user/models/regular-user.model';
import { UuidValidationPipe } from 'src/infrastructure/pipes/uuid.pipe';
import { SignDocumentContractByVolunteerUsecase } from 'src/usecases/documents/new_contracts/sign-document-contract-by-volunteer.usecase';
-import { SignDocumentContractDto } from './dto/SignDocumentContract.dto';
+import { RejectDocumentContractByVolunteerUsecase } from 'src/usecases/documents/new_contracts/reject-document-contact-by-volunteer.usecase';
+import { SignDocumentContractByVolunteerDto } from './dto/SignDocumentContractByVolunteer.dto';
+import { RejectDocumentContractByVolunteerDto } from './dto/RejectDocumentContractByVolunteer.dto';
// @UseGuards(MobileJwtAuthGuard, ContractVolunteerGuard)
@UseGuards(MobileJwtAuthGuard)
@@ -15,12 +17,13 @@ import { SignDocumentContractDto } from './dto/SignDocumentContract.dto';
export class MobileDocumentsContractController {
constructor(
private readonly signDocumentContractByVolunteerUsecase: SignDocumentContractByVolunteerUsecase,
+ private readonly rejectDocumentContractByVolunteerUsecase: RejectDocumentContractByVolunteerUsecase,
) {}
@ApiParam({ name: 'contractId', type: 'string' })
@Patch(':contractId/sign')
async sign(
- @Body() body: SignDocumentContractDto,
+ @Body() body: SignDocumentContractByVolunteerDto,
@ExtractUser() { id }: IRegularUserModel,
@Param('contractId', UuidValidationPipe) contractId: string,
): Promise {
@@ -34,4 +37,19 @@ export class MobileDocumentsContractController {
return contract;
}
+
+ @ApiParam({ name: 'contractId', type: 'string' })
+ @Patch(':contractId/reject')
+ async reject(
+ @Body() body: RejectDocumentContractByVolunteerDto,
+ @ExtractUser() { id }: IRegularUserModel,
+ @Param('contractId', UuidValidationPipe) contractId: string,
+ ): Promise {
+ await this.rejectDocumentContractByVolunteerUsecase.execute({
+ contractId,
+ userId: id,
+ organizationId: body.organizationId,
+ rejectionReason: body.reason,
+ });
+ }
}
diff --git a/backend/src/api/_mobile/documents/dto/RejectDocumentContractByVolunteer.dto.ts b/backend/src/api/_mobile/documents/dto/RejectDocumentContractByVolunteer.dto.ts
new file mode 100644
index 000000000..2351e022e
--- /dev/null
+++ b/backend/src/api/_mobile/documents/dto/RejectDocumentContractByVolunteer.dto.ts
@@ -0,0 +1,13 @@
+import { IsString, IsOptional } from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+
+export class RejectDocumentContractByVolunteerDto {
+ @ApiProperty({ description: 'Organization ID' })
+ @IsString()
+ organizationId: string;
+
+ @ApiProperty({ description: 'Reason for rejecting the contract' })
+ @IsString()
+ @IsOptional()
+ reason?: string;
+}
diff --git a/backend/src/api/_mobile/documents/dto/SignDocumentContract.dto.ts b/backend/src/api/_mobile/documents/dto/SignDocumentContractByVolunteer.dto.ts
similarity index 91%
rename from backend/src/api/_mobile/documents/dto/SignDocumentContract.dto.ts
rename to backend/src/api/_mobile/documents/dto/SignDocumentContractByVolunteer.dto.ts
index 9a513d964..843847ef3 100644
--- a/backend/src/api/_mobile/documents/dto/SignDocumentContract.dto.ts
+++ b/backend/src/api/_mobile/documents/dto/SignDocumentContractByVolunteer.dto.ts
@@ -1,7 +1,7 @@
import { IsString, IsOptional, IsUUID } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
-export class SignDocumentContractDto {
+export class SignDocumentContractByVolunteerDto {
@ApiProperty({ description: 'The ID of the organization' })
@IsUUID()
organizationId: string;
diff --git a/backend/src/usecases/documents/new_contracts/reject-document-contact-by-volunteer.usecase.ts b/backend/src/usecases/documents/new_contracts/reject-document-contact-by-volunteer.usecase.ts
new file mode 100644
index 000000000..3dfaf6b11
--- /dev/null
+++ b/backend/src/usecases/documents/new_contracts/reject-document-contact-by-volunteer.usecase.ts
@@ -0,0 +1,145 @@
+import { Injectable } from '@nestjs/common';
+import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface';
+import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service';
+import { DocumentContractStatus } from 'src/modules/documents/enums/contract-status.enum';
+import { ContractExceptionMessages } from 'src/modules/documents/exceptions/contract.exceptions';
+import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade';
+import { VolunteerFacade } from 'src/modules/volunteer/services/volunteer.facade';
+
+// ┌─────────────────────────────────────────────────────────────────────────┐
+// │ Business Rules for RejectDocumentContractByVolunteerUsecase: │
+// │ │
+// │ 1. Volunteer Authentication: │
+// │ - The volunteer must exist and be associated with the given │
+// │ organization. │
+// │ - If the volunteer is not found or not part of the organization, │
+// │ throw a not found exception. │
+// │ │
+// │ 2. Contract Validation: │
+// │ - The contract must exist and be in the PENDING_VOLUNTEER_SIGNATURE │
+// │ status. │
+// │ - The contract must be assigned to the current volunteer. │
+// │ - The contract must belong to the user's organization. │
+// │ - If any of these conditions are not met, throw a not found │
+// │ exception. │
+// │ │
+// │ 4. Contract Update: │
+// │ - Update the contract status to REJECTED_BY_VOLUNTEER. │
+// │ │
+// │ 5. Error Handling: │
+// │ - Any failures in the process should throw appropriate exceptions. │
+// │ - Use the ExceptionsService to handle and throw standardized │
+// │ exceptions. │
+// │ │
+// │ 6. Transactional Integrity: // TODO: Implement this │
+// │ - Ensure that all database operations are performed atomically. │
+// │ - If any part of the process fails, all changes should be rolled │
+// │ back. │
+// │ │
+// │ 7. Audit Trail: // TODO: Implement this │
+// │ - Track the rejection event in an Actions Archive for auditing │
+// │ purposes. │
+// │ - Store the rejection reason in the Actions Archive. │
+// │ │
+// │ 8. Authorization: │
+// │ - Ensure that only the assigned volunteer can reject their own │
+// │ contract. │
+// │ │
+// │ 9. Notification: │
+// │ - Notify relevant parties (e.g., NGO administrators) about the │
+// │ contract rejection. │
+// │ │
+// │ 10. Data Validation: │
+// │ - Validate the format and content of the rejection reason before │
+// │ processing. │
+// └─────────────────────────────────────────────────────────────────────────┘
+
+@Injectable()
+export class RejectDocumentContractByVolunteerUsecase
+ implements IUseCaseService
+{
+ constructor(
+ private readonly documentContractFacade: DocumentContractFacade,
+ private readonly volunteerFacade: VolunteerFacade,
+ private readonly exceptionService: ExceptionsService,
+ ) {}
+
+ public async execute({
+ contractId,
+ userId,
+ organizationId,
+ rejectionReason,
+ }: {
+ contractId: string;
+ userId: string;
+ organizationId: string;
+ rejectionReason: string;
+ }): Promise {
+ /* ┌─────────────────────────────────────────────────────────────────────┐
+ * │ Verify volunteer existence: │
+ * │ │
+ * │ 1. Volunteer must exist and be part of the organization │
+ * │ │
+ * │ This ensures that the volunteer is valid and authorized to reject. │
+ * └─────────────────────────────────────────────────────────────────────┘
+ */
+ const volunteer = await this.volunteerFacade.find({
+ userId: userId,
+ organizationId,
+ });
+ if (!volunteer) {
+ this.exceptionService.notFoundException({
+ message: 'Volunteer is not part of the organization',
+ code_error: 'VOLUNTEER_NOT_PART_OF_ORGANIZATION',
+ });
+ }
+
+ /* ┌─────────────────────────────────────────────────────────────────────┐
+ * │ Verify contract existence and eligibility: │
+ * │ │
+ * │ 1. Status must be PENDING_VOLUNTEER_SIGNATURE │
+ * │ 2. Contract is assigned to the current volunteer │
+ * │ 3. Contract belongs to the user's organization │
+ * │ │
+ * │ This ensures that the contract is valid and authorized to be │
+ * │ rejected. │
+ * └─────────────────────────────────────────────────────────────────────┘
+ */
+ const contractExists = await this.documentContractFacade.exists({
+ id: contractId,
+ volunteerId: volunteer.id,
+ organizationId,
+ status: DocumentContractStatus.PENDING_VOLUNTEER_SIGNATURE,
+ });
+
+ if (!contractExists) {
+ this.exceptionService.notFoundException(
+ ContractExceptionMessages.CONTRACT_002,
+ );
+ }
+
+ /* ┌─────────────────────────────────────────────────────────────────────┐
+ * │ Update contract status: │
+ * │ │
+ * │ 1. Update the contract status to REJECTED_VOLUNTEER. │
+ * └─────────────────────────────────────────────────────────────────────┘
+ */
+ await this.documentContractFacade.update(contractId, {
+ status: DocumentContractStatus.REJECTED_VOLUNTEER,
+ });
+
+ /* ┌─────────────────────────────────────────────────────────────────────┐
+ * │ Audit trail logging: │
+ * │ │
+ * │ 1. Log the contract rejection event in the Actions Archive together │
+ * │ with the rejection reason. │
+ * └─────────────────────────────────────────────────────────────────────┘
+ */
+ // TODO: Implement audit trail logging
+ console.log('rejectionReason', rejectionReason);
+
+ // TODO: Implement notification to relevant parties
+
+ return;
+ }
+}
diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts
index 7fc95532e..e309a3f6c 100644
--- a/backend/src/usecases/use-case.module.ts
+++ b/backend/src/usecases/use-case.module.ts
@@ -144,6 +144,7 @@ import { CreateDocumentContractUsecase } from './documents/new_contracts/create-
import { GetManyDocumentContractsUsecase } from './documents/new_contracts/get-many-document-contracts.usecase';
import { GetManyDocumentTemplatesUsecase } from './documents/new_contracts/get-many-document-templates.usecase';
import { SignDocumentContractByVolunteerUsecase } from './documents/new_contracts/sign-document-contract-by-volunteer.usecase';
+import { RejectDocumentContractByVolunteerUsecase } from './documents/new_contracts/reject-document-contact-by-volunteer.usecase';
const providers = [
// Organization
@@ -294,6 +295,7 @@ const providers = [
CreateDocumentContractUsecase,
GetManyDocumentContractsUsecase,
SignDocumentContractByVolunteerUsecase,
+ RejectDocumentContractByVolunteerUsecase,
// Notifications
UpdateSettingsUsecase,
// Testing PDFs
From 47b08005646cbe1dc9149e7ee76e8a7e38d09fc8 Mon Sep 17 00:00:00 2001
From: luciatugui
Date: Thu, 12 Sep 2024 14:18:47 +0300
Subject: [PATCH 31/55] feat: [Contracts] - scroll to input with errors in
Identity Data
---
mobile/src/assets/locales/en/translation.json | 8 +-
mobile/src/assets/locales/ro/translation.json | 8 +-
mobile/src/layouts/FormLayout.tsx | 7 +-
mobile/src/screens/IdentityData.tsx | 312 +++++++++++-------
4 files changed, 200 insertions(+), 135 deletions(-)
diff --git a/mobile/src/assets/locales/en/translation.json b/mobile/src/assets/locales/en/translation.json
index 12bcec03a..36de111f4 100644
--- a/mobile/src/assets/locales/en/translation.json
+++ b/mobile/src/assets/locales/en/translation.json
@@ -266,7 +266,8 @@
},
"issue_date": {
"label": "Date of issue",
- "required": "Date of issue is mandatory"
+ "required": "Date of issue is mandatory",
+ "future": "The date of issue cannot be later than the current date"
},
"issued_by": {
"label": "Issued by",
@@ -275,7 +276,8 @@
},
"expiration_date": {
"label": "Expiry date",
- "required": "Expiry date is mandatory"
+ "required": "Expiry date is mandatory",
+ "future": "The expiration date cannot be before the current date"
},
"guardian": {
"name": {
@@ -842,4 +844,4 @@
"confirm": "Confirm Deletion",
"error": "There was an error trying to delete your account. Please contact us at {{value}} to finalize the process."
}
-}
+}
\ No newline at end of file
diff --git a/mobile/src/assets/locales/ro/translation.json b/mobile/src/assets/locales/ro/translation.json
index 55bab3f04..841ed908e 100644
--- a/mobile/src/assets/locales/ro/translation.json
+++ b/mobile/src/assets/locales/ro/translation.json
@@ -266,7 +266,8 @@
},
"issue_date": {
"label": "Data emitere",
- "required": "Data emiterii este obligatorie"
+ "required": "Data emiterii este obligatorie",
+ "future": "Data emiterii nu poate fi ulterioară datei curente"
},
"issued_by": {
"label": "Eliberat de",
@@ -275,7 +276,8 @@
},
"expiration_date": {
"label": "Data expirare",
- "required": "Data expirării este obligatorie"
+ "required": "Data expirării este obligatorie",
+ "future": "Data expirării nu poate fi înaintea datei curente"
},
"guardian": {
"name": {
@@ -843,4 +845,4 @@
"confirm": "Confirmă Ștergerea",
"error": "A apărut o eroare la ștergerea contului. Contactează-ne la adresa de e-mail {{value}}, și te vom ajuta să finalizezi procesul."
}
-}
+}
\ No newline at end of file
diff --git a/mobile/src/layouts/FormLayout.tsx b/mobile/src/layouts/FormLayout.tsx
index 38a7f1bec..f7aae0ce2 100644
--- a/mobile/src/layouts/FormLayout.tsx
+++ b/mobile/src/layouts/FormLayout.tsx
@@ -1,13 +1,14 @@
-import React from 'react';
+import React, { forwardRef } from 'react';
import { ScrollView, StyleSheet } from 'react-native';
interface FormLayoutProps {
children: React.ReactNode;
}
-const FormLayout = ({ children }: FormLayoutProps) => {
+const FormLayout = forwardRef(({ children }, ref) => {
return (
{
{children}
);
-};
+});
export default FormLayout;
diff --git a/mobile/src/screens/IdentityData.tsx b/mobile/src/screens/IdentityData.tsx
index 14d17d0a9..1bba460f9 100644
--- a/mobile/src/screens/IdentityData.tsx
+++ b/mobile/src/screens/IdentityData.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import PageLayout from '../layouts/PageLayout';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
@@ -20,6 +20,7 @@ import { useUserProfile } from '../store/profile/profile.selector';
import { usePaddingTop } from '../hooks/usePaddingTop';
import { differenceInYears, parseISO } from 'date-fns';
import { UserPersonalDataPayload } from '../services/user/user.api';
+import { findNodeHandle, ScrollView, View } from 'react-native';
export type IdentityDataFormTypes = {
identityDocumentCNP: string;
@@ -82,9 +83,6 @@ const formSchema = (isUserOver16: boolean, userBirthday: Date | undefined) =>
return true;
}
const cnpBirthday = getBirthdayFromCNP(value);
- // TODO: remove this after testing
- console.log('cnpBirthday: 🎂', cnpBirthday);
- console.log('userBirthday: 🎂', userBirthday);
return cnpBirthday ? cnpBirthday.getTime() === new Date(userBirthday).getTime() : true;
},
),
@@ -105,13 +103,15 @@ const formSchema = (isUserOver16: boolean, userBirthday: Date | undefined) =>
.max(100, `${i18n.t('identity_data:form.address.max', { value: 100 })}`),
identityDocumentIssueDate: yup
.date()
- .required(`${i18n.t('identity_data:form.issue_date.required')}`),
+ .required(`${i18n.t('identity_data:form.issue_date.required')}`)
+ .max(new Date(), `${i18n.t('identity_data:form.issue_date.future')}`),
identityDocumentIssuedBy: yup
.string()
.required(`${i18n.t('identity_data:form.issued_by.required')}`),
identityDocumentExpirationDate: yup
.date()
- .required(`${i18n.t('identity_data:form.expiration_date.required')}`),
+ .required(`${i18n.t('identity_data:form.expiration_date.required')}`)
+ .min(new Date(), `${i18n.t('identity_data:form.expiration_date.future')}`),
guardianName: yup.string().when([], {
is: () => !isUserOver16,
then: (schema) => schema.required(`${i18n.t('identity_data:form.guardian.name.required')}`),
@@ -220,6 +220,8 @@ const getBirthdayFromCNP = (cnp: string): Date | null => {
const IdentityData = ({ navigation, route }: any) => {
const paddingTop = usePaddingTop();
const { t } = useTranslation('identity_data');
+ const scrollViewRef = useRef(null);
+ const inputRefs = useRef<{ [key: string]: View | null }>({});
const { userProfile } = useUserProfile();
@@ -284,6 +286,34 @@ const IdentityData = ({ navigation, route }: any) => {
return () => subscription.unsubscribe();
}, [watch, userProfile?.birthday]);
+ const registerInputRef = (name: keyof IdentityDataFormTypes) => (ref: View | null) => {
+ inputRefs.current[name] = ref;
+ };
+
+ // scroll to the first input that has an error
+ useEffect(() => {
+ if (Object.keys(errors).length > 0) {
+ // check if there are any errors and get the key of the first error
+ const firstErrorKey = Object.keys(errors)[0];
+ const errorInput = inputRefs.current[firstErrorKey];
+
+ if (errorInput && scrollViewRef.current) {
+ const scrollViewHandle = findNodeHandle(scrollViewRef.current);
+ if (scrollViewHandle) {
+ errorInput.measureLayout(
+ scrollViewHandle,
+ (_, y) => {
+ // measure the layout of the error input relative to the scroll view
+ // scroll to the error input, with a 50-pixel offset for better visibility
+ scrollViewRef.current?.scrollTo({ y: y - 50, animated: true });
+ },
+ () => console.log('measurement failed'),
+ );
+ }
+ }
+ }
+ }, [errors]);
+
const onPrivacyPolicyPress = () => {
Linking.openURL(`${process.env.EXPO_PUBLIC_PRIVACY_POLICY_LINK}`);
};
@@ -317,131 +347,161 @@ const IdentityData = ({ navigation, route }: any) => {
}}
headerStyle={{ paddingTop }}
>
-
+
{`${t('description')}`}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{!isUserOver16 && (
<>
{`${t('legal_gardian_data_required')}`}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
)}
From aacec28909a3d336833652e74d1f0177e91b44b2 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Thu, 12 Sep 2024 14:43:58 +0300
Subject: [PATCH 32/55] feat: [Contracts] API - get many contracts from an
organization for a volunteer
---
.../documents-contract.controller.ts | 28 ++++++++++-
.../dto/GetManyContractsByVolunteer.dto.ts | 12 +++++
backend/src/api/public/public.controller.ts | 2 +-
.../document-contract-list-view.entity.ts | 12 +++--
.../document-contract-list-view.repository.ts | 7 ---
.../generate-pdfs.usecase.ts | 0
...document-contracts-by-volunteer.usecase.ts | 50 +++++++++++++++++++
backend/src/usecases/use-case.module.ts | 4 +-
8 files changed, 100 insertions(+), 15 deletions(-)
create mode 100644 backend/src/api/_mobile/documents/dto/GetManyContractsByVolunteer.dto.ts
rename backend/src/usecases/documents/{ => new_contracts}/generate-pdfs.usecase.ts (100%)
create mode 100644 backend/src/usecases/documents/new_contracts/get-many-document-contracts-by-volunteer.usecase.ts
diff --git a/backend/src/api/_mobile/documents/documents-contract.controller.ts b/backend/src/api/_mobile/documents/documents-contract.controller.ts
index 5026ed709..ccc01183f 100644
--- a/backend/src/api/_mobile/documents/documents-contract.controller.ts
+++ b/backend/src/api/_mobile/documents/documents-contract.controller.ts
@@ -1,4 +1,4 @@
-import { Body, Param, Patch, UseGuards } from '@nestjs/common';
+import { Body, Get, Param, Patch, Query, UseGuards } from '@nestjs/common';
import { Controller } from '@nestjs/common';
import { ApiBearerAuth, ApiParam } from '@nestjs/swagger';
import { MobileJwtAuthGuard } from 'src/modules/auth/guards/jwt-mobile.guard';
@@ -9,6 +9,11 @@ import { SignDocumentContractByVolunteerUsecase } from 'src/usecases/documents/n
import { RejectDocumentContractByVolunteerUsecase } from 'src/usecases/documents/new_contracts/reject-document-contact-by-volunteer.usecase';
import { SignDocumentContractByVolunteerDto } from './dto/SignDocumentContractByVolunteer.dto';
import { RejectDocumentContractByVolunteerDto } from './dto/RejectDocumentContractByVolunteer.dto';
+import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
+import { GetManyDocumentContractsByVolunteerUsecase } from 'src/usecases/documents/new_contracts/get-many-document-contracts-by-volunteer.usecase';
+import { GetManyContractsByVolunteerDto } from './dto/GetManyContractsByVolunteer.dto';
+import { DocumentContractListViewItemPresenter } from 'src/api/documents/presenters/document-contract-list-view-item.presenter';
+import { PaginatedPresenter } from 'src/infrastructure/presenters/generic-paginated.presenter';
// @UseGuards(MobileJwtAuthGuard, ContractVolunteerGuard)
@UseGuards(MobileJwtAuthGuard)
@@ -18,8 +23,29 @@ export class MobileDocumentsContractController {
constructor(
private readonly signDocumentContractByVolunteerUsecase: SignDocumentContractByVolunteerUsecase,
private readonly rejectDocumentContractByVolunteerUsecase: RejectDocumentContractByVolunteerUsecase,
+ private readonly getManyDocumentContractsByVolunteerUsecase: GetManyDocumentContractsByVolunteerUsecase,
) {}
+ // Get all contracts for a volunteer
+ @Get()
+ async findMany(
+ @ExtractUser() { id: userId }: IRegularUserModel,
+ @Query() query: GetManyContractsByVolunteerDto,
+ ): Promise> {
+ const contracts =
+ await this.getManyDocumentContractsByVolunteerUsecase.execute({
+ ...query,
+ userId,
+ });
+
+ return new PaginatedPresenter({
+ ...contracts,
+ items: contracts.items.map(
+ (contract) => new DocumentContractListViewItemPresenter(contract),
+ ),
+ });
+ }
+
@ApiParam({ name: 'contractId', type: 'string' })
@Patch(':contractId/sign')
async sign(
diff --git a/backend/src/api/_mobile/documents/dto/GetManyContractsByVolunteer.dto.ts b/backend/src/api/_mobile/documents/dto/GetManyContractsByVolunteer.dto.ts
new file mode 100644
index 000000000..e9eaf2310
--- /dev/null
+++ b/backend/src/api/_mobile/documents/dto/GetManyContractsByVolunteer.dto.ts
@@ -0,0 +1,12 @@
+import { IsUUID } from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+import { BasePaginationFilterDto } from 'src/infrastructure/base/base-pagination-filter.dto';
+
+export class GetManyContractsByVolunteerDto extends BasePaginationFilterDto {
+ @ApiProperty({
+ description: 'The ID of the organization',
+ example: '123e4567-e89b-12d3-a456-426614174000',
+ })
+ @IsUUID()
+ organizationId: string;
+}
diff --git a/backend/src/api/public/public.controller.ts b/backend/src/api/public/public.controller.ts
index c8e34a778..cb4f58571 100644
--- a/backend/src/api/public/public.controller.ts
+++ b/backend/src/api/public/public.controller.ts
@@ -1,7 +1,7 @@
import { Controller, Get } from '@nestjs/common';
import { SkipThrottle } from '@nestjs/throttler';
import { APP_VERSION } from 'src/common/constants/version';
-import { GeneratePDFsUseCase } from 'src/usecases/documents/generate-pdfs.usecase';
+import { GeneratePDFsUseCase } from 'src/usecases/documents/new_contracts/generate-pdfs.usecase';
@Controller('public')
export class PublicController {
diff --git a/backend/src/modules/documents/entities/document-contract-list-view.entity.ts b/backend/src/modules/documents/entities/document-contract-list-view.entity.ts
index 7233e5735..4a7fbad2d 100644
--- a/backend/src/modules/documents/entities/document-contract-list-view.entity.ts
+++ b/backend/src/modules/documents/entities/document-contract-list-view.entity.ts
@@ -1,4 +1,4 @@
-import { ViewColumn, ViewEntity } from 'typeorm';
+import { Column, ViewColumn, ViewEntity } from 'typeorm';
import { DocumentContractStatus } from '../enums/contract-status.enum';
@ViewEntity('DocumentContractListView', {
@@ -8,7 +8,7 @@ import { DocumentContractStatus } from '../enums/contract-status.enum';
document_contract.document_number as document_number,
document_contract.status as status,
document_contract.document_start_date as document_start_date,
- document_contract.document_end_date as document_end_date,
+ document_contract.document_end_date as document_end_date,
document_contract.file_path AS document_file_path,
organization.id AS organization_id,
organization.name AS organization_name,
@@ -25,13 +25,15 @@ export class DocumentContractListViewEntity {
@ViewColumn({ name: 'document_id' })
documentId: string;
- @ViewColumn({ name: 'document_number' })
+ @Column({ name: 'document_number' })
documentNumber: string;
- @ViewColumn({ name: 'document_start_date' })
+ // Use Column instead of ViewColumn because of https://github.com/typeorm/typeorm/issues/4320. Date was returned as Timestamp
+ @Column({ name: 'document_start_date', type: 'date' })
documentStartDate: Date;
- @ViewColumn({ name: 'document_end_date' })
+ // Use Column instead of ViewColumn because of https://github.com/typeorm/typeorm/issues/4320. Date was returned as Timestamp
+ @Column({ name: 'document_end_date', type: 'date' })
documentEndDate: Date;
@ViewColumn({ name: 'document_file_path' })
diff --git a/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts b/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts
index 23aefd6db..1a6a7607d 100644
--- a/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts
+++ b/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts
@@ -20,13 +20,6 @@ export class DocumentContractListViewRepository extends RepositoryWithPagination
private readonly documentContractListViewRepository: Repository,
) {
super(documentContractListViewRepository);
-
- // this.findMany({
- // limit: 10,
- // page: 1,
- // organizationId: '7f005461-07c3-4693-a85d-40d31db43a4c',
- // status: DocumentContractStatus.APPROVED,
- // }).then(console.log);
}
async findMany(
diff --git a/backend/src/usecases/documents/generate-pdfs.usecase.ts b/backend/src/usecases/documents/new_contracts/generate-pdfs.usecase.ts
similarity index 100%
rename from backend/src/usecases/documents/generate-pdfs.usecase.ts
rename to backend/src/usecases/documents/new_contracts/generate-pdfs.usecase.ts
diff --git a/backend/src/usecases/documents/new_contracts/get-many-document-contracts-by-volunteer.usecase.ts b/backend/src/usecases/documents/new_contracts/get-many-document-contracts-by-volunteer.usecase.ts
new file mode 100644
index 000000000..8a9e18eb9
--- /dev/null
+++ b/backend/src/usecases/documents/new_contracts/get-many-document-contracts-by-volunteer.usecase.ts
@@ -0,0 +1,50 @@
+import { Injectable } from '@nestjs/common';
+import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface';
+import { IBasePaginationFilterModel } from 'src/infrastructure/base/base-pagination-filter.model';
+import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
+import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service';
+import { IDocumentContractListViewModel } from 'src/modules/documents/models/document-contract-list-view.model';
+import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade';
+import { VolunteerExceptionMessages } from 'src/modules/volunteer/exceptions/volunteer.exceptions';
+import { VolunteerFacade } from 'src/modules/volunteer/services/volunteer.facade';
+
+@Injectable()
+export class GetManyDocumentContractsByVolunteerUsecase
+ implements IUseCaseService>
+{
+ constructor(
+ private readonly documentContractFacade: DocumentContractFacade,
+ private readonly volunteerFacade: VolunteerFacade,
+ private readonly exceptionService: ExceptionsService,
+ ) {}
+
+ public async execute({
+ userId,
+ organizationId,
+ ...paginationOptions
+ }: {
+ userId: string;
+ organizationId: string;
+ } & IBasePaginationFilterModel): Promise<
+ Pagination
+ > {
+ // 1. Find the volunteerId based on userId and organizationId
+ const volunteer = await this.volunteerFacade.find({
+ userId: userId,
+ organizationId,
+ });
+
+ if (!volunteer) {
+ this.exceptionService.notFoundException(
+ VolunteerExceptionMessages.VOLUNTEER_001,
+ );
+ }
+
+ // 2. Find the document contracts based on the volunteerId
+ return this.documentContractFacade.findMany({
+ ...paginationOptions,
+ organizationId,
+ volunteerId: volunteer.id,
+ });
+ }
+}
diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts
index e309a3f6c..e77caf878 100644
--- a/backend/src/usecases/use-case.module.ts
+++ b/backend/src/usecases/use-case.module.ts
@@ -136,7 +136,6 @@ 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';
import { SyncWithOngHubUseCaseService } from './organization/sync-with-ngohub.usecase';
import { CreateDocumentTemplateUsecase } from './documents/new_contracts/create-document-template.usecase';
import { GetOneDocumentTemplateUseCase } from './documents/new_contracts/get-one-document-template.usecase';
@@ -145,6 +144,8 @@ import { GetManyDocumentContractsUsecase } from './documents/new_contracts/get-m
import { GetManyDocumentTemplatesUsecase } from './documents/new_contracts/get-many-document-templates.usecase';
import { SignDocumentContractByVolunteerUsecase } from './documents/new_contracts/sign-document-contract-by-volunteer.usecase';
import { RejectDocumentContractByVolunteerUsecase } from './documents/new_contracts/reject-document-contact-by-volunteer.usecase';
+import { GeneratePDFsUseCase } from './documents/new_contracts/generate-pdfs.usecase';
+import { GetManyDocumentContractsByVolunteerUsecase } from './documents/new_contracts/get-many-document-contracts-by-volunteer.usecase';
const providers = [
// Organization
@@ -296,6 +297,7 @@ const providers = [
GetManyDocumentContractsUsecase,
SignDocumentContractByVolunteerUsecase,
RejectDocumentContractByVolunteerUsecase,
+ GetManyDocumentContractsByVolunteerUsecase,
// Notifications
UpdateSettingsUsecase,
// Testing PDFs
From 902bc71ff25810b5def262e6d518ef6b29804ce5 Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Thu, 12 Sep 2024 15:06:04 +0300
Subject: [PATCH 33/55] feat: [Contracts] API - Get one contract for volunteer
---
.../documents-contract.controller.ts | 18 +++++++
.../document-contract-list-view.model.ts | 5 ++
.../document-contract-list-view.repository.ts | 9 ++++
.../services/document-contract.facade.ts | 7 +++
...document-contract-for-volunteer.usecase.ts | 50 +++++++++++++++++++
backend/src/usecases/use-case.module.ts | 2 +
6 files changed, 91 insertions(+)
create mode 100644 backend/src/usecases/documents/new_contracts/get-one-document-contract-for-volunteer.usecase.ts
diff --git a/backend/src/api/_mobile/documents/documents-contract.controller.ts b/backend/src/api/_mobile/documents/documents-contract.controller.ts
index ccc01183f..fc380974d 100644
--- a/backend/src/api/_mobile/documents/documents-contract.controller.ts
+++ b/backend/src/api/_mobile/documents/documents-contract.controller.ts
@@ -14,6 +14,7 @@ import { GetManyDocumentContractsByVolunteerUsecase } from 'src/usecases/documen
import { GetManyContractsByVolunteerDto } from './dto/GetManyContractsByVolunteer.dto';
import { DocumentContractListViewItemPresenter } from 'src/api/documents/presenters/document-contract-list-view-item.presenter';
import { PaginatedPresenter } from 'src/infrastructure/presenters/generic-paginated.presenter';
+import { GetOneDocumentContractForVolunteerUsecase } from 'src/usecases/documents/new_contracts/get-one-document-contract-for-volunteer.usecase';
// @UseGuards(MobileJwtAuthGuard, ContractVolunteerGuard)
@UseGuards(MobileJwtAuthGuard)
@@ -24,6 +25,7 @@ export class MobileDocumentsContractController {
private readonly signDocumentContractByVolunteerUsecase: SignDocumentContractByVolunteerUsecase,
private readonly rejectDocumentContractByVolunteerUsecase: RejectDocumentContractByVolunteerUsecase,
private readonly getManyDocumentContractsByVolunteerUsecase: GetManyDocumentContractsByVolunteerUsecase,
+ private readonly getOneDocumentContractForVolunteerUsecase: GetOneDocumentContractForVolunteerUsecase,
) {}
// Get all contracts for a volunteer
@@ -46,6 +48,22 @@ export class MobileDocumentsContractController {
});
}
+ @Get(':contractId')
+ async findOne(
+ @ExtractUser() { id }: IRegularUserModel,
+ @Param('contractId', UuidValidationPipe) contractId: string,
+ @Query('organizationId', UuidValidationPipe) organizationId: string,
+ ): Promise {
+ const contract =
+ await this.getOneDocumentContractForVolunteerUsecase.execute({
+ documentContractId: contractId,
+ userId: id,
+ organizationId,
+ });
+
+ return new DocumentContractListViewItemPresenter(contract);
+ }
+
@ApiParam({ name: 'contractId', type: 'string' })
@Patch(':contractId/sign')
async sign(
diff --git a/backend/src/modules/documents/models/document-contract-list-view.model.ts b/backend/src/modules/documents/models/document-contract-list-view.model.ts
index f5509a08f..b32cc46df 100644
--- a/backend/src/modules/documents/models/document-contract-list-view.model.ts
+++ b/backend/src/modules/documents/models/document-contract-list-view.model.ts
@@ -15,6 +15,11 @@ export interface IDocumentContractListViewModel {
organizationName: string;
}
+export type FindOneDocumentContractListViewOptions = {
+ documentId: string;
+ volunteerId: string;
+};
+
export type FindManyDocumentContractListViewOptions =
IBasePaginationFilterModel & {
organizationId: string;
diff --git a/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts b/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts
index 1a6a7607d..211f73d7d 100644
--- a/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts
+++ b/backend/src/modules/documents/repositories/document-contract-list-view.repository.ts
@@ -9,6 +9,7 @@ import { Injectable } from '@nestjs/common';
import {
DocumentContractListViewTransformer,
FindManyDocumentContractListViewOptions,
+ FindOneDocumentContractListViewOptions,
IDocumentContractListViewModel,
} from '../models/document-contract-list-view.model';
import { OrderDirection } from 'src/common/enums/order-direction.enum';
@@ -76,4 +77,12 @@ export class DocumentContractListViewRepository extends RepositoryWithPagination
DocumentContractListViewTransformer.fromEntity,
);
}
+
+ async findOne(
+ options: FindOneDocumentContractListViewOptions,
+ ): Promise {
+ return this.documentContractListViewRepository.findOne({
+ where: options,
+ });
+ }
}
diff --git a/backend/src/modules/documents/services/document-contract.facade.ts b/backend/src/modules/documents/services/document-contract.facade.ts
index 63f8b9c73..18bedf720 100644
--- a/backend/src/modules/documents/services/document-contract.facade.ts
+++ b/backend/src/modules/documents/services/document-contract.facade.ts
@@ -9,6 +9,7 @@ import {
import { DocumentContractListViewRepository } from '../repositories/document-contract-list-view.repository';
import {
FindManyDocumentContractListViewOptions,
+ FindOneDocumentContractListViewOptions,
IDocumentContractListViewModel,
} from '../models/document-contract-list-view.model';
import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
@@ -42,6 +43,12 @@ export class DocumentContractFacade {
return this.documentContractListViewRepository.findMany(options);
}
+ async findOneForVolunteer(
+ options: FindOneDocumentContractListViewOptions,
+ ): Promise {
+ return this.documentContractListViewRepository.findOne(options);
+ }
+
async update(
id: string,
updates: UpdateDocumentContractOptions,
diff --git a/backend/src/usecases/documents/new_contracts/get-one-document-contract-for-volunteer.usecase.ts b/backend/src/usecases/documents/new_contracts/get-one-document-contract-for-volunteer.usecase.ts
new file mode 100644
index 000000000..c688f4a07
--- /dev/null
+++ b/backend/src/usecases/documents/new_contracts/get-one-document-contract-for-volunteer.usecase.ts
@@ -0,0 +1,50 @@
+import { Injectable } from '@nestjs/common';
+import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service';
+import { DocumentContractListViewEntity } from 'src/modules/documents/entities/document-contract-list-view.entity';
+import { ContractExceptionMessages } from 'src/modules/documents/exceptions/contract.exceptions';
+import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade';
+import { VolunteerExceptionMessages } from 'src/modules/volunteer/exceptions/volunteer.exceptions';
+import { VolunteerFacade } from 'src/modules/volunteer/services/volunteer.facade';
+
+@Injectable()
+export class GetOneDocumentContractForVolunteerUsecase {
+ constructor(
+ private readonly documentContractFacade: DocumentContractFacade,
+ private readonly volunteerFacade: VolunteerFacade,
+ private readonly exceptionService: ExceptionsService,
+ ) {}
+
+ async execute({
+ documentContractId,
+ userId,
+ organizationId,
+ }: {
+ documentContractId: string;
+ userId: string;
+ organizationId: string;
+ }): Promise {
+ const volunteer = await this.volunteerFacade.find({
+ userId: userId,
+ organizationId,
+ });
+
+ if (!volunteer) {
+ this.exceptionService.notFoundException(
+ VolunteerExceptionMessages.VOLUNTEER_001,
+ );
+ }
+
+ const contract = await this.documentContractFacade.findOneForVolunteer({
+ documentId: documentContractId,
+ volunteerId: volunteer.id,
+ });
+
+ if (!contract) {
+ this.exceptionService.notFoundException(
+ ContractExceptionMessages.CONTRACT_002,
+ );
+ }
+
+ return contract;
+ }
+}
diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts
index e77caf878..2cb925f0e 100644
--- a/backend/src/usecases/use-case.module.ts
+++ b/backend/src/usecases/use-case.module.ts
@@ -146,6 +146,7 @@ import { SignDocumentContractByVolunteerUsecase } from './documents/new_contract
import { RejectDocumentContractByVolunteerUsecase } from './documents/new_contracts/reject-document-contact-by-volunteer.usecase';
import { GeneratePDFsUseCase } from './documents/new_contracts/generate-pdfs.usecase';
import { GetManyDocumentContractsByVolunteerUsecase } from './documents/new_contracts/get-many-document-contracts-by-volunteer.usecase';
+import { GetOneDocumentContractForVolunteerUsecase } from './documents/new_contracts/get-one-document-contract-for-volunteer.usecase';
const providers = [
// Organization
@@ -298,6 +299,7 @@ const providers = [
SignDocumentContractByVolunteerUsecase,
RejectDocumentContractByVolunteerUsecase,
GetManyDocumentContractsByVolunteerUsecase,
+ GetOneDocumentContractForVolunteerUsecase,
// Notifications
UpdateSettingsUsecase,
// Testing PDFs
From b4bd65dea49e3df6cd5b8001a2f081438d75de8e Mon Sep 17 00:00:00 2001
From: Andrew Radulescu
Date: Thu, 12 Sep 2024 15:27:58 +0300
Subject: [PATCH 34/55] feat: [Contracts] API - Approve contract by NGO
---
.../documents/document-contract.controller.ts | 26 +++++++++---
.../services/document-contract.facade.ts | 9 +++++
...pprove-document-contract-by-ngo.usecase.ts | 40 +++++++++++++++++++
backend/src/usecases/use-case.module.ts | 2 +
4 files changed, 71 insertions(+), 6 deletions(-)
create mode 100644 backend/src/usecases/documents/new_contracts/approve-document-contract-by-ngo.usecase.ts
diff --git a/backend/src/api/documents/document-contract.controller.ts b/backend/src/api/documents/document-contract.controller.ts
index cffdad15e..d2e37b652 100644
--- a/backend/src/api/documents/document-contract.controller.ts
+++ b/backend/src/api/documents/document-contract.controller.ts
@@ -1,4 +1,13 @@
-import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
+import {
+ Body,
+ Controller,
+ Get,
+ Param,
+ Patch,
+ Post,
+ Query,
+ UseGuards,
+} from '@nestjs/common';
import { CreateDocumentContractUsecase } from 'src/usecases/documents/new_contracts/create-document-contract.usecase';
import { CreateDocumentContractDto } from './dto/create-document-contract.dto';
import { IAdminUserModel } from 'src/modules/user/models/admin-user.model';
@@ -13,6 +22,8 @@ import {
PaginatedPresenter,
} from 'src/infrastructure/presenters/generic-paginated.presenter';
import { GetManyDocumentContractsDto } from './dto/get-many-document-contracts.dto';
+import { UuidValidationPipe } from 'src/infrastructure/pipes/uuid.pipe';
+import { ApproveDocumentContractByNgoUsecase } from 'src/usecases/documents/new_contracts/approve-document-contract-by-ngo.usecase';
@ApiBearerAuth()
@UseGuards(WebJwtAuthGuard)
@@ -21,6 +32,7 @@ export class DocumentContractController {
constructor(
private readonly createDocumentContractUsecase: CreateDocumentContractUsecase,
private readonly getManyDocumentContractsUsecase: GetManyDocumentContractsUsecase,
+ private readonly approveDocumentContractByNgoUsecase: ApproveDocumentContractByNgoUsecase,
) {}
@Post()
@@ -56,9 +68,11 @@ export class DocumentContractController {
});
}
- /* TODO: GET /documents/contracts/check?year={year}&documentNumber={documentNumber}
- CHECK IF A CONTRACT ALREADY EXISTS FOR THE GIVEN YEAR AND DOCUMENT NUMBER IN THE SAME ORGANIZATION
- RETURN TRUE IF IT EXISTS, FALSE OTHERWISE
- USED TO PREVENT DUPLICATE DOCUMENT NUMBERS IN THE SAME YEAR
- */
+ @Patch(':id/approve')
+ async approveDocumentContract(
+ @Param('id', UuidValidationPipe) id: string,
+ @ExtractUser() { organizationId }: IAdminUserModel,
+ ): Promise {
+ await this.approveDocumentContractByNgoUsecase.execute(id, organizationId);
+ }
}
diff --git a/backend/src/modules/documents/services/document-contract.facade.ts b/backend/src/modules/documents/services/document-contract.facade.ts
index 18bedf720..c754b7d85 100644
--- a/backend/src/modules/documents/services/document-contract.facade.ts
+++ b/backend/src/modules/documents/services/document-contract.facade.ts
@@ -13,6 +13,7 @@ import {
IDocumentContractListViewModel,
} from '../models/document-contract-list-view.model';
import { Pagination } from 'src/infrastructure/base/repository-with-pagination.class';
+import { DocumentContractStatus } from '../enums/contract-status.enum';
@Injectable()
export class DocumentContractFacade {
@@ -21,6 +22,14 @@ export class DocumentContractFacade {
private readonly documentContractListViewRepository: DocumentContractListViewRepository,
) {}
+ async approveDocumentContractByNGO(
+ documentContractId: string,
+ ): Promise {
+ return this.documentContractRepository.update(documentContractId, {
+ status: DocumentContractStatus.PENDING_NGO_REPRESENTATIVE_SIGNATURE,
+ });
+ }
+
async create(
newDocumentContract: CreateDocumentContractOptions,
): Promise {
diff --git a/backend/src/usecases/documents/new_contracts/approve-document-contract-by-ngo.usecase.ts b/backend/src/usecases/documents/new_contracts/approve-document-contract-by-ngo.usecase.ts
new file mode 100644
index 000000000..43e4c2c1c
--- /dev/null
+++ b/backend/src/usecases/documents/new_contracts/approve-document-contract-by-ngo.usecase.ts
@@ -0,0 +1,40 @@
+import { Injectable } from '@nestjs/common';
+import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service';
+import { DocumentContractStatus } from 'src/modules/documents/enums/contract-status.enum';
+import { ContractExceptionMessages } from 'src/modules/documents/exceptions/contract.exceptions';
+import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade';
+
+@Injectable()
+export class ApproveDocumentContractByNgoUsecase {
+ constructor(
+ private readonly documentContractFacade: DocumentContractFacade,
+ private readonly exceptionService: ExceptionsService,
+ ) {}
+
+ async execute(
+ documentContractId: string,
+ organizationId: string,
+ ): Promise {
+ const exists = await this.documentContractFacade.exists({
+ id: documentContractId,
+ organizationId,
+ status: DocumentContractStatus.PENDING_APPROVAL_NGO,
+ });
+
+ if (!exists) {
+ this.exceptionService.notFoundException(
+ ContractExceptionMessages.CONTRACT_002,
+ );
+ }
+ try {
+ await this.documentContractFacade.approveDocumentContractByNGO(
+ documentContractId,
+ );
+ } catch (error) {
+ this.exceptionService.internalServerErrorException({
+ message: `Error while approving the contract by NGO ${error?.message}`,
+ code_error: 'APPROVE_DOCUMENT_CONTRACT_BY_NGO_001',
+ });
+ }
+ }
+}
diff --git a/backend/src/usecases/use-case.module.ts b/backend/src/usecases/use-case.module.ts
index 2cb925f0e..b19dd4b61 100644
--- a/backend/src/usecases/use-case.module.ts
+++ b/backend/src/usecases/use-case.module.ts
@@ -147,6 +147,7 @@ import { RejectDocumentContractByVolunteerUsecase } from './documents/new_contra
import { GeneratePDFsUseCase } from './documents/new_contracts/generate-pdfs.usecase';
import { GetManyDocumentContractsByVolunteerUsecase } from './documents/new_contracts/get-many-document-contracts-by-volunteer.usecase';
import { GetOneDocumentContractForVolunteerUsecase } from './documents/new_contracts/get-one-document-contract-for-volunteer.usecase';
+import { ApproveDocumentContractByNgoUsecase } from './documents/new_contracts/approve-document-contract-by-ngo.usecase';
const providers = [
// Organization
@@ -300,6 +301,7 @@ const providers = [
RejectDocumentContractByVolunteerUsecase,
GetManyDocumentContractsByVolunteerUsecase,
GetOneDocumentContractForVolunteerUsecase,
+ ApproveDocumentContractByNgoUsecase,
// Notifications
UpdateSettingsUsecase,
// Testing PDFs
From 1faeed97af71e45794012ba0f558664438a12a9d Mon Sep 17 00:00:00 2001
From: Dragos-Paul Strat
Date: Fri, 13 Sep 2024 16:53:16 +0300
Subject: [PATCH 35/55] feat: [contracts] wip generate contract
---
frontend/.prettierrc.json | 1 +
frontend/src/common/constants/routes.ts | 13 +-
.../common/interfaces/template.interface.ts | 29 ++
.../src/common/utils/volunteer-data.util.ts | 81 ++++
frontend/src/components/CardsExample.tsx | 163 -------
frontend/src/components/ContractCard.tsx | 82 ++--
.../src/components/ContractCardHeader.tsx | 23 +-
.../src/components/DataTableComponent.tsx | 7 +
.../components/DocumentContractFillCards.tsx | 115 +++++
.../src/components/DocumentTemplateTable.tsx | 154 +++++++
.../components/DocumentVolunteersTable.tsx | 399 ++++++++++++++++++
frontend/src/components/Signatures.tsx | 54 +--
frontend/src/components/Stepper.tsx | 78 ++--
.../DocumentTemplatesTableWithQueryParams.tsx | 40 ++
...DocumentVolunteersTableWithQueryParams.tsx | 60 +++
frontend/src/pages/GenerateContract.tsx | 166 ++------
frontend/src/pages/StepperExample.tsx | 68 ---
frontend/src/routes/Router.tsx | 3 -
.../documents-templates.api.ts | 20 +
.../documents-templates.service.ts | 57 ++-
20 files changed, 1094 insertions(+), 519 deletions(-)
create mode 100644 frontend/src/common/utils/volunteer-data.util.ts
delete mode 100644 frontend/src/components/CardsExample.tsx
create mode 100644 frontend/src/components/DocumentContractFillCards.tsx
create mode 100644 frontend/src/components/DocumentTemplateTable.tsx
create mode 100644 frontend/src/components/DocumentVolunteersTable.tsx
create mode 100644 frontend/src/containers/query/DocumentTemplatesTableWithQueryParams.tsx
create mode 100644 frontend/src/containers/query/DocumentVolunteersTableWithQueryParams.tsx
delete mode 100644 frontend/src/pages/StepperExample.tsx
diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json
index 0515424ac..68e609f44 100644
--- a/frontend/.prettierrc.json
+++ b/frontend/.prettierrc.json
@@ -2,6 +2,7 @@
"printWidth": 100,
"singleQuote": true,
"trailingComma": "all",
+ "tabWidth": 2,
"semi": true,
"bracketSpacing": true,
"endOfLine": "auto",
diff --git a/frontend/src/common/constants/routes.ts b/frontend/src/common/constants/routes.ts
index 3dffefb30..0c516b8ed 100644
--- a/frontend/src/common/constants/routes.ts
+++ b/frontend/src/common/constants/routes.ts
@@ -63,29 +63,24 @@ export const ROUTES: IRoute[] = [
childRoutes: [
{
id: 61,
- name: i18n.t('general:contracts'),
+ name: i18n.t('general:contracts') + ' - OLD',
href: 'documents/contracts',
},
{
id: 62,
- name: 'Contracte * NEW *',
+ name: 'Contracte',
href: 'documents/templates',
},
{
id: 63,
- name: 'Creează template * NEW *',
+ name: 'Creează template',
href: 'documents/templates/create',
},
{
id: 64,
- name: 'Generează contract * NEW *',
+ name: 'Generează contract',
href: 'documents/templates/contracts/generate',
},
- {
- id: 65,
- name: 'stepper',
- href: 'documents/templates/stepper_example',
- },
],
},
{
diff --git a/frontend/src/common/interfaces/template.interface.ts b/frontend/src/common/interfaces/template.interface.ts
index b00cfde6f..adc2f6f8e 100644
--- a/frontend/src/common/interfaces/template.interface.ts
+++ b/frontend/src/common/interfaces/template.interface.ts
@@ -1,5 +1,34 @@
+import { IOrganization } from './organization.interface';
+import { IUser } from './user.interface';
+
export interface ITemplate {
id: string;
name: string;
path: string;
}
+
+export interface IDocumentTemplateListItem {
+ id: string;
+ name: string;
+ usageCount: string;
+ lastUsage: string;
+ createdById: string;
+ createdByName: string;
+ createdOn: string;
+}
+
+export interface IOrganizationData {
+ officialName: string;
+ registeredOffice: string;
+ CUI: string;
+ legalRepresentativeName: string;
+ legalRepresentativeRole: string;
+}
+
+export interface IDocumentTemplate {
+ id: string;
+ name: string;
+ documentTerms: string; // HTML
+ createdByAdmin: Pick;
+ organizationData: IOrganizationData;
+}
diff --git a/frontend/src/common/utils/volunteer-data.util.ts b/frontend/src/common/utils/volunteer-data.util.ts
new file mode 100644
index 000000000..76fd0153e
--- /dev/null
+++ b/frontend/src/common/utils/volunteer-data.util.ts
@@ -0,0 +1,81 @@
+import { IVolunteer } from '../interfaces/volunteer.interface';
+
+export interface VolunteerDataCheck {
+ isIncomplete: boolean;
+ missingInfo: string;
+}
+
+const fieldTranslations: { [key: string]: string } = {
+ guardian: 'Tutore legal',
+ cnp: 'CNP',
+ identityDocumentSeries: 'Serie buletin',
+ identityDocumentNumber: 'Număr buletin',
+ address: 'Adresă',
+ email: 'Email',
+ phone: 'Telefon',
+ name: 'Nume',
+ identityDocumentIssueDate: 'Data emiterii buletinului',
+ identityDocumentExpirationDate: 'Data expirării buletinului',
+ identityDocumentIssuedBy: 'Emitentul documentului',
+};
+
+export const checkIsVolunteerDataIncomplete = (volunteer: IVolunteer): VolunteerDataCheck => {
+ const missingVolunteerFields: string[] = [];
+ const missingGuardianFields: string[] = [];
+
+ if (volunteer?.user?.age < 18) {
+ if (!volunteer?.user?.userPersonalData?.legalGuardian) {
+ missingGuardianFields.push('Necompletat');
+ } else {
+ const legalGuardian = volunteer.user.userPersonalData.legalGuardian;
+ const guardianFields = [
+ 'cnp',
+ 'identityDocumentSeries',
+ 'identityDocumentNumber',
+ 'address',
+ 'email',
+ 'phone',
+ 'name',
+ ];
+ guardianFields.forEach((field) => {
+ if (!legalGuardian[field as keyof typeof legalGuardian]) {
+ missingGuardianFields.push(`${fieldTranslations[field]}`);
+ }
+ });
+ }
+ }
+
+ const personalDataFields = [
+ 'identityDocumentSeries',
+ 'identityDocumentNumber',
+ 'identityDocumentIssueDate',
+ 'identityDocumentExpirationDate',
+ 'identityDocumentIssuedBy',
+ 'cnp',
+ 'address',
+ ];
+
+ personalDataFields.forEach((field) => {
+ if (
+ !volunteer?.user?.userPersonalData?.[field as keyof typeof volunteer.user.userPersonalData]
+ ) {
+ missingVolunteerFields.push(fieldTranslations[field]);
+ }
+ });
+
+ const volunteerInfo =
+ missingVolunteerFields.length > 0
+ ? `Informatii necompletate Voluntar: ${missingVolunteerFields.join(', ')}`
+ : '';
+ const guardianInfo =
+ missingGuardianFields.length > 0
+ ? `Informatii necompletate Tutore: ${missingGuardianFields.join(', ')}`
+ : '';
+
+ const missingInfo = [volunteerInfo, guardianInfo].filter(Boolean).join('\n\n');
+
+ return {
+ isIncomplete: missingVolunteerFields.length > 0 || missingGuardianFields.length > 0,
+ missingInfo,
+ };
+};
diff --git a/frontend/src/components/CardsExample.tsx b/frontend/src/components/CardsExample.tsx
deleted file mode 100644
index f61d235dd..000000000
--- a/frontend/src/components/CardsExample.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import React, { useState } from 'react';
-import { ContractCard } from './ContractCard';
-import { FieldValues, useForm } from 'react-hook-form';
-import { AutoFillContractCard } from './AutoFillContractCard';
-
-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 = () => {
- 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 (
- <>
-
-
-
- {items.map((item, index) => (
-
- ))}
-
- >
- );
-};
diff --git a/frontend/src/components/ContractCard.tsx b/frontend/src/components/ContractCard.tsx
index b8df5b22d..79671e626 100644
--- a/frontend/src/components/ContractCard.tsx
+++ b/frontend/src/components/ContractCard.tsx
@@ -6,60 +6,40 @@ 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;
- };
-}
+import { IVolunteer } from '../common/interfaces/volunteer.interface';
+import { IDocumentTemplate } from '../common/interfaces/template.interface';
+import { format } from 'date-fns';
interface ContractCardProps {
- data: { contract: IMockContract; volunteer: IMockVolunteer };
+ volunteer: IVolunteer,
+ template: IDocumentTemplate,
initialNumber?: string;
initialDate?: Date | null;
initialPeriod?: [Date | null, Date | null];
+ isOpen?: boolean;
+ onDelete: (id: string) => void;
}
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,
+ volunteer,
+ template,
initialNumber,
initialDate,
initialPeriod,
+ onDelete,
+ isOpen = false,
}: ContractCardProps) => {
const { t } = useTranslation(['doc_templates', 'general']);
// contract card states
- const [open, setOpen] = useState(false);
+ const [open, setOpen] = useState(isOpen);
const [edit, setEdit] = useState(false);
- // 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 isVolunteerDataIncomplete: boolean = false;
+
const { control, handleSubmit, setValue } = useForm({
defaultValues: {
contractNumber: initialNumber || '',
@@ -101,7 +81,6 @@ export const ContractCard = ({
: [dotsString, dotsString],
);
- // on submit -> update the values in the contract preview
const onSubmit = (data: FieldValues) => {
if (data.contractNumber) {
setContractNumber(data.contractNumber);
@@ -125,19 +104,18 @@ export const ContractCard = ({
{open && (
-
+
{/* datele contractului */}
{t('contract_data')}
- {/* //todo: icon at the end of the input??? */}
{t('template_preview.p2.between')}{' '}
- {organization?.name || `[${t('organization_name')}]`}
+ {template?.organizationData?.officialName || `[${t('organization_name')}]`}
{' '}
{t('template_preview.p2.address')}{' '}
- {organization?.address || `[${t('organization_address')}]`}{' '}
+ {template?.organizationData?.registeredOffice || `[${t('organization_address')}]`}{' '}
{t('template_preview.p2.identified')}
{' '}
- {organization?.cui || `[${t('organization_cui')}]`}
+ {template?.organizationData?.CUI || `[${t('organization_cui')}]`}
{', '}
{t('template_preview.p2.represented_by')}{' '}
{' '}
- {organization?.legalReprezentativeFullName || `[${t('legal_rep_name')}]`}
+ {template?.organizationData?.legalRepresentativeName || `[${t('legal_rep_name')}]`}
{', '}
{t('template_preview.p2.as')}{' '}
- {organization?.legalReprezentativeRole || `[${t('legal_rep_role')}]`}{' '}
+ {template?.organizationData?.legalRepresentativeRole || `[${t('legal_rep_role')}]`}{' '}
{t('template_preview.p2.named')}{' '}
{t('template_preview.p2.organization')} {' '}
@@ -221,12 +199,12 @@ export const ContractCard = ({
{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}
+ {volunteer.user.name} ,{' '}
+ {t('template_preview.p3.lives')} {volunteer.user.userPersonalData?.address}, {t('template_preview.p3.cnp')}{' '}
+ {volunteer.user.userPersonalData?.cnp} ,{' '}
+ {t('template_preview.p3.legitimate')} {volunteer.user.userPersonalData?.identityDocumentSeries} {t('template_preview.p3.no')}{' '}
+ {volunteer.user.userPersonalData?.identityDocumentNumber}, {t('template_preview.p3.by')} {volunteer.user.userPersonalData?.identityDocumentIssuedBy},{' '}
+ {t('template_preview.p3.at_date')} {volunteer.user.userPersonalData?.identityDocumentExpirationDate && format(volunteer.user.userPersonalData?.identityDocumentExpirationDate, 'dd/MM/yyyy')},
{', '}
{t('template_preview.p3.named')}{' '}
{t('template_preview.p3.volunteer')} {' '}
@@ -244,9 +222,9 @@ export const ContractCard = ({
{t('contract_terms.title')}
- {contractTerms &&
}
+ {template.documentTerms &&
}
-
+
)}
diff --git a/frontend/src/components/ContractCardHeader.tsx b/frontend/src/components/ContractCardHeader.tsx
index b71d9a3b6..d3a6a0819 100644
--- a/frontend/src/components/ContractCardHeader.tsx
+++ b/frontend/src/components/ContractCardHeader.tsx
@@ -1,15 +1,15 @@
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';
import LoadingContent from './LoadingContent';
+import { IVolunteer } from '../common/interfaces/volunteer.interface';
interface ContractCardHeaderProps {
open: boolean;
setOpen: React.Dispatch
>;
- volunteer: IMockVolunteer;
- // todo: onDelete
+ volunteer: IVolunteer;
+ onDelete: (id: string) => void;
isLoading?: boolean;
isError?: boolean;
isErrorText?: string;
@@ -19,18 +19,17 @@ export const ContractCardHeader = ({
open,
setOpen,
volunteer,
+ onDelete,
isLoading,
isError,
isErrorText,
}: ContractCardHeaderProps) => {
- // todo: remove isError from here and use the right thing instead
- // todo: error state
if (isLoading) {
return (
@@ -38,10 +37,10 @@ export const ContractCardHeader = ({
}
return (
-
+
console.log('delete item')}
+ onClick={() => onDelete(volunteer.id)}
>
@@ -51,9 +50,9 @@ export const ContractCardHeader = ({
onClick={() => setOpen(!open)}
>
-
+
- {volunteer.name}
+ {volunteer.user.name}
{isError && (
<>
@@ -61,10 +60,10 @@ export const ContractCardHeader = ({
width={20}
height={20}
color="red"
- data-tooltip-id={`error-tooltip-${volunteer.cnp}`}
+ data-tooltip-id={`error-tooltip-${volunteer.user.userPersonalData?.cnp}`}
/>
{
selectableRows?: boolean;
selectableRowsSingle?: boolean;
onSelectedRowsChange?: (selectedRows: T[]) => void;
+ selectableRowSelected?: (row: T) => boolean;
+ selectableRowDisabled?: (row: T) => boolean;
onSort?: (selectedColumn: TableColumn, sortDirection: SortOrder, sortedRows: T[]) => void;
defaultSortFieldId?: string | number;
defaultSortAsc?: boolean;
+
}
const DataTableComponent = ({
@@ -41,6 +44,8 @@ const DataTableComponent = ({
selectableRows,
selectableRowsSingle,
onSelectedRowsChange,
+ selectableRowSelected,
+ selectableRowDisabled,
defaultSortFieldId,
defaultSortAsc,
}: DataTableProps) => {
@@ -76,6 +81,8 @@ const DataTableComponent = ({
defaultSortAsc={defaultSortAsc}
selectableRows={selectableRows}
selectableRowsSingle={selectableRowsSingle}
+ selectableRowSelected={selectableRowSelected}
+ selectableRowDisabled={selectableRowDisabled}
onSelectedRowsChange={(selected) =>
onSelectedRowsChange && onSelectedRowsChange(selected.selectedRows)
}
diff --git a/frontend/src/components/DocumentContractFillCards.tsx b/frontend/src/components/DocumentContractFillCards.tsx
new file mode 100644
index 000000000..448f1bc09
--- /dev/null
+++ b/frontend/src/components/DocumentContractFillCards.tsx
@@ -0,0 +1,115 @@
+import React, { useEffect, useState } from 'react';
+import { ContractCard } from './ContractCard';
+import { FieldValues, useForm } from 'react-hook-form';
+import { AutoFillContractCard } from './AutoFillContractCard';
+import { IDocumentTemplate, IDocumentTemplateListItem } from '../common/interfaces/template.interface';
+import { IVolunteer } from '../common/interfaces/volunteer.interface';
+import { useDocumentTemplateByIdQuery } from '../services/documents-templates/documents-templates.service';
+import LoadingContent from './LoadingContent';
+// Add this import
+import ConfirmationModal from './ConfirmationModal'; // Adjust the import path as needed
+
+interface DocumentContractFillCardsProps {
+ volunteers: IVolunteer[];
+ template: IDocumentTemplateListItem;
+ setSelectedVolunteers: (volunteers: IVolunteer[]) => void;
+}
+
+export const DocumentContractFillCards = ({ volunteers, template, setSelectedVolunteers }: DocumentContractFillCardsProps) => {
+ 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 [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [volunteerToDelete, setVolunteerToDelete] = useState(null);
+
+
+ const { data: templateData, isLoading: isLoadingTemplate } = useDocumentTemplateByIdQuery(template.id);
+
+ useEffect(() => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }, []);
+
+ 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);
+ }
+ };
+
+ if (isLoadingTemplate) {
+ return
+ }
+
+ const onDelete = (id: string) => {
+ setVolunteerToDelete(id);
+ setIsDeleteModalOpen(true);
+ };
+
+ const handleConfirmDelete = () => {
+ if (volunteerToDelete) {
+ setSelectedVolunteers(volunteers.filter(volunteer => volunteer.id !== volunteerToDelete));
+ setIsDeleteModalOpen(false);
+ setVolunteerToDelete(null);
+ }
+ };
+
+ const handleCancelDelete = () => {
+ setIsDeleteModalOpen(false);
+ setVolunteerToDelete(null);
+ };
+
+
+ return (
+ <>
+
+
+
+ {volunteers.map((item, index) => (
+
+ ))}
+
+
+ {isDeleteModalOpen && (
+
+ )}
+ >
+ );
+};
diff --git a/frontend/src/components/DocumentTemplateTable.tsx b/frontend/src/components/DocumentTemplateTable.tsx
new file mode 100644
index 000000000..825ec8cf5
--- /dev/null
+++ b/frontend/src/components/DocumentTemplateTable.tsx
@@ -0,0 +1,154 @@
+import React, { useCallback, useEffect, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import CardBody from './CardBody';
+import DataTableComponent from './DataTableComponent';
+import Card from '../layouts/CardLayout';
+import CardHeader from './CardHeader';
+import { IDocumentTemplateListItem } from '../common/interfaces/template.interface';
+import { useDocumentTemplatesQuery } from '../services/documents-templates/documents-templates.service';
+import { OrderDirection } from '../common/enums/order-direction.enum';
+import { format } from 'date-fns';
+import { DocumentTemplatesProps } from '../containers/query/DocumentTemplatesTableWithQueryParams';
+import { SortOrder, TableColumn } from 'react-data-table-component';
+
+const DocumentTemplatesTableHeader = [
+ {
+ id: 'name',
+ name: 'Nume',
+ sortable: true,
+ grow: 4,
+ minWidth: '9rem',
+ selector: (row: IDocumentTemplateListItem) => row.name,
+ },
+ {
+ id: 'uses',
+ name: 'Utilizări',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ selector: (row: IDocumentTemplateListItem) => row.usageCount,
+ },
+ {
+ id: 'last_used',
+ name: 'Ultima utilizare',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ selector: (row: IDocumentTemplateListItem) => row.lastUsage ? format(row.lastUsage, 'dd/MM/yyyy') : '-',
+ },
+ {
+ id: 'created_by',
+ name: 'Creat de',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ selector: (row: IDocumentTemplateListItem) => row.createdByName,
+ },
+ {
+ id: 'created_at',
+ name: 'Data creării',
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ selector: (row: IDocumentTemplateListItem) => format(row.createdOn, 'dd/MM/yyyy'),
+ },
+];
+
+
+export const DocumentTemplateTable = ({ query, setQuery, selectedTemplate, onSelectTemplate }: DocumentTemplatesProps) => {
+ const { t } = useTranslation(['volunteering_contracts', 'stepper']);
+ const firstRender = useRef(true);
+
+ useEffect(() => { firstRender.current = false }, []);
+
+ const { data: templates, isLoading: isLoadingDocumentTemplates } = useDocumentTemplatesQuery({
+ limit: 10,
+ page: 1,
+ orderBy: 'name',
+ orderDirection: OrderDirection.ASC,
+ });
+
+ const handleOnSelectTemplate = (templates: IDocumentTemplateListItem[]) => {
+ if (templates.length === 0) {
+ onSelectTemplate(templates[0]);
+ } else if (templates[0].id !== selectedTemplate?.id) {
+ onSelectTemplate(templates[0]);
+ }
+ };
+
+ const defaultSelectedRows = useCallback((row: IDocumentTemplateListItem) => {
+ return row.id === selectedTemplate?.id;
+ }, [selectedTemplate]);
+
+ // We're doing this because of a bug in DataTableComponent
+ // https://github.com/jbetancur/react-data-table-component/issues/930
+ // https://github.com/jbetancur/react-data-table-component/issues/955
+
+ // While some fixes exist we should be able to also unselect the row, thus removing props after the first render is the only way that works.
+ const selectProps = {
+ ...(firstRender.current && {
+ selectableRowSelected: defaultSelectedRows
+ }),
+ ...(!firstRender.current && {
+ onSelectedRowsChange: handleOnSelectTemplate,
+ }),
+ };
+
+ // pagination
+ const onRowsPerPageChange = (limit: number) => {
+ setQuery(
+ {
+ limit,
+ page: 1,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onChangePage = (page: number) => {
+ setQuery(
+ {
+ page,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onSort = (column: TableColumn, direction: SortOrder) => {
+ setQuery(
+ {
+ orderBy: column.id as string,
+ orderDirection:
+ direction.toLocaleUpperCase() === OrderDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC,
+ },
+ 'replaceIn',
+ );
+ };
+
+ return (
+
+
+ {t('templates')}
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/DocumentVolunteersTable.tsx b/frontend/src/components/DocumentVolunteersTable.tsx
new file mode 100644
index 000000000..5b1276751
--- /dev/null
+++ b/frontend/src/components/DocumentVolunteersTable.tsx
@@ -0,0 +1,399 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import PageLayout from '../layouts/PageLayout';
+import Card from '../layouts/CardLayout';
+import CardBody from './CardBody';
+import DataTableComponent from './DataTableComponent';
+import i18n from '../common/config/i18n';
+import { SortOrder, TableColumn } from 'react-data-table-component';
+import { OrderDirection } from '../common/enums/order-direction.enum';
+import { SelectItem } from './Select';
+import { AgeRangeOptions, formatLocation } from '../common/utils/utils';
+import { useErrorToast } from '../hooks/useToast';
+import { InternalErrors } from '../common/errors/internal-errors.class';
+import MediaCell from './MediaCell';
+import {
+ useVolunteersQuery,
+} from '../services/volunteer/volunteer.service';
+import { IVolunteer } from '../common/interfaces/volunteer.interface';
+import { VolunteerStatus } from '../common/enums/volunteer-status.enum';
+import DataTableFilters from './DataTableFilters';
+import DateRangePicker from './DateRangePicker';
+import LocationSelect from '../containers/LocationSelect';
+import { ListItem } from '../common/interfaces/list-item.interface';
+import OrganizationStructureSelect from '../containers/OrganizationStructureSelect';
+import { DivisionType } from '../common/enums/division-type.enum';
+import { AgeRangeEnum } from '../common/enums/age-range.enum';
+import SelectFilter from '../containers/SelectFilter';
+import CardHeader from './CardHeader';
+import { DocumentVolunteersProps } from '../containers/query/DocumentVolunteersTableWithQueryParams';
+import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
+import { Tooltip } from 'react-tooltip';
+import { checkIsVolunteerDataIncomplete, VolunteerDataCheck } from '../common/utils/volunteer-data.util';
+
+
+const ActiveVolunteersTableHeader = [
+ {
+ id: 'user.firstName',
+ name: i18n.t('general:name'),
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ cell: (row: IVolunteer & { completionStatus: VolunteerDataCheck }) => (
+
+
+
+
+
+
+
+ {row.completionStatus?.isIncomplete && }
+ {row.completionStatus?.isIncomplete && }
+
+
+ )
+ },
+ {
+ id: 'department.name',
+ name: i18n.t('volunteers:department_and_role'),
+ sortable: true,
+ grow: 1,
+ minWidth: '9rem',
+ selector: (row: IVolunteer) =>
+ row.profile?.department || row?.profile?.role
+ ? `${row.profile?.role?.name || ''}${row.profile?.role && row.profile?.department ? '\n' : ''
+ }${row.profile?.department?.name || ''}`
+ : '-',
+ },
+ {
+ id: 'location.name',
+ name: i18n.t('volunteers:location'),
+ sortable: true,
+ grow: 1,
+ minWidth: '5rem',
+ selector: (row: IVolunteer) => formatLocation(row.user.location),
+ },
+ {
+ id: 'volunteerProfile.email',
+ name: i18n.t('general:contact'),
+ sortable: true,
+ grow: 1,
+ minWidth: '14rem',
+ selector: (row: IVolunteer) =>
+ row.profile ? `${row.profile?.email}\n${row.user?.phone}` : '-',
+ },
+];
+
+
+const DocumentVolunteersTable = ({ query, setQuery, selectedVolunteers, setSelectedVolunteers }: DocumentVolunteersProps) => {
+ // filters
+ const [location, setLocation] = useState();
+ const [branch, setBranch] = useState>();
+ const [department, setDepartment] = useState>();
+ const [role, setRole] = useState>();
+
+ const firstRender = useRef(true);
+
+ useEffect(() => { firstRender.current = false }, []);
+
+ useEffect(() => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }, []);
+
+ const {
+ data: volunteers,
+ isLoading: isVolunteersLoading,
+ error: volunteersError,
+ } = useVolunteersQuery(
+ query?.volunteerStatus as VolunteerStatus,
+ query?.limit as number,
+ query?.page as number,
+ query?.orderBy as string,
+ query?.orderDirection as OrderDirection,
+ query?.search as string,
+ query?.age as AgeRangeEnum,
+ query?.branch,
+ query?.department,
+ query?.role,
+ query?.location && query?.location[0],
+ query?.location && query?.location[1],
+ query?.createdOnStart as Date,
+ query?.createdOnEnd as Date,
+ );
+
+ const volunteersWithChecks = useMemo(() => {
+ return volunteers?.items?.map((volunteer: IVolunteer) => {
+ const completionStatus = checkIsVolunteerDataIncomplete(volunteer);
+ return {
+ ...volunteer,
+ completionStatus,
+ };
+ });
+ }, [volunteers]);
+
+ useEffect(() => {
+ if (volunteersError)
+ useErrorToast(
+ InternalErrors.VOLUNTEER_ERRORS.getError(volunteersError.response?.data.code_error),
+ );
+ }, [volunteersError]);
+
+ const handleOnSelectVolunteers = (volunteers: IVolunteer[]) => {
+ if (volunteers.length === 0) {
+ setSelectedVolunteers(volunteers);
+ } else if (volunteers.length !== selectedVolunteers?.length) {
+ setSelectedVolunteers(volunteers);
+ }
+ };
+
+ const defaultSelectedRows = useCallback((row: IVolunteer) => {
+ return selectedVolunteers?.some((volunteer: IVolunteer) => volunteer.id === row.id);
+ }, [selectedVolunteers]);
+
+ // We're doing this because of a bug in DataTableComponent
+ // https://github.com/jbetancur/react-data-table-component/issues/930
+ // https://github.com/jbetancur/react-data-table-component/issues/955
+
+ // While some fixes exist we should be able to also unselect the row, thus removing props after the first render is the only way that works.
+ const selectProps = {
+ ...(firstRender.current && {
+ selectableRowSelected: defaultSelectedRows
+ }),
+ ...(!firstRender.current && {
+ onSelectedRowsChange: handleOnSelectVolunteers,
+ }),
+ };
+
+
+
+ // pagination
+ const onRowsPerPageChange = (limit: number) => {
+ setQuery(
+ {
+ limit,
+ page: 1,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onChangePage = (page: number) => {
+ setQuery(
+ {
+ page,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onSort = (column: TableColumn, direction: SortOrder) => {
+ setQuery(
+ {
+ orderBy: column.id as string,
+ orderDirection:
+ direction.toLocaleUpperCase() === OrderDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onSearch = (search: string) => {
+ setQuery(
+ {
+ search,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onSetBranchFilter = (branch: SelectItem | undefined) => {
+ setBranch(branch);
+ setQuery(
+ {
+ branch: branch?.value,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onSetDepartmentFilter = (department: SelectItem | undefined) => {
+ setDepartment(department);
+ setQuery(
+ {
+ department: department?.value,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onSetRoleFilter = (role: SelectItem | undefined) => {
+ setRole(role);
+ setQuery(
+ {
+ role: role?.value,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onCreatedOnRangeChange = ([createdOnStart, createdOnEnd]: Date[]) => {
+ setQuery(
+ {
+ createdOnStart,
+ createdOnEnd,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onLocationChange = (location: ListItem) => {
+ setLocation(location);
+ setQuery(
+ {
+ location: location.label.split(', '),
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onAgeRangeChange = (selectedRange: SelectItem | undefined) => {
+ setQuery(
+ {
+ age: selectedRange?.key,
+ },
+ 'replaceIn',
+ );
+ };
+
+ const onResetFilters = () => {
+ setLocation(undefined);
+ setBranch(undefined);
+ setDepartment(undefined);
+ setRole(undefined);
+ setQuery(
+ {
+ volunteerStatus: query.volunteerStatus,
+ location: undefined,
+ branch: undefined,
+ department: undefined,
+ role: undefined,
+ age: undefined,
+ createdOnEnd: undefined,
+ createdOnStart: undefined,
+ },
+ 'replaceIn',
+ );
+ };
+
+
+
+ return (
+
+
+ {
+ const [createdOnStart, createdOnEnd] = range;
+ onCreatedOnRangeChange([createdOnStart as Date, createdOnEnd as Date]);
+ }}
+ value={
+ query?.createdOnStart && query?.createdOnEnd
+ ? [query?.createdOnStart, query?.createdOnEnd]
+ : undefined
+ }
+ id="created-on-range__picker"
+ />
+
+
+
+
+
+
+
+ {i18n.t('side_menu:options.volunteers_list')}
+
+ {query?.volunteerStatus === VolunteerStatus.ACTIVE && (
+ row.completionStatus.isIncomplete}
+ selectableRows
+ paginationPerPage={query.limit as number}
+ paginationTotalRows={volunteers?.meta?.totalItems}
+ paginationDefaultPage={query.page as number}
+ onChangeRowsPerPage={onRowsPerPageChange}
+ onChangePage={onChangePage}
+ onSort={onSort}
+ />
+ )}
+
+
+
+ );
+};
+
+export default DocumentVolunteersTable;
diff --git a/frontend/src/components/Signatures.tsx b/frontend/src/components/Signatures.tsx
index 095c7b7bb..a4119fd2e 100644
--- a/frontend/src/components/Signatures.tsx
+++ b/frontend/src/components/Signatures.tsx
@@ -2,34 +2,18 @@ import React from 'react';
import { InfoParagraph } from './InfoParagraph';
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';
+import { IOrganizationData } from '../common/interfaces/template.interface';
+import { IVolunteer } from '../common/interfaces/volunteer.interface';
interface SignatureProps {
- volunteer?: IMockVolunteer;
+ volunteer?: IVolunteer;
+ organization: IOrganizationData;
}
-export const Signatures = ({ volunteer }: SignatureProps) => {
+export const Signatures = ({ volunteer, organization }: SignatureProps) => {
const { t } = useTranslation('doc_templates');
- const {
- data: organization,
- isLoading: isLoadingOrganization,
- isError: isErrorOrganization,
- isFetching,
- refetch,
- } = useOrganizationQuery();
- if (isLoadingOrganization) {
- return ;
- }
-
- if (isErrorOrganization) {
- return ;
- }
-
- const isOrganizationNameMissing = !organization?.name;
+ const isOrganizationNameMissing = !organization?.officialName;
return (
@@ -39,10 +23,10 @@ export const Signatures = ({ volunteer }: SignatureProps) => {
{t('template_preview.p2.organization')}{' '}
{volunteer ? (
-
{organization?.name}
+
{organization?.officialName}
) : (
)}
@@ -52,10 +36,10 @@ export const Signatures = ({ volunteer }: SignatureProps) => {
{' '}
{t('represented_by')}
{volunteer ? (
-
{organization?.legalReprezentativeFullName}
+
{organization?.legalRepresentativeName}
) : (
)}
@@ -66,7 +50,7 @@ export const Signatures = ({ volunteer }: SignatureProps) => {
signatureTitle={{t('volunteer.volunteer')}
}
p={
volunteer ? (
- volunteer.name
+ volunteer.user.name
) : (
)
@@ -82,22 +66,22 @@ export const Signatures = ({ volunteer }: SignatureProps) => {
signatureTitle={{t('legal_representative')}
}
p={
- {volunteer?.legalRepresentative?.name ? (
-
{volunteer.legalRepresentative.name}
+ {volunteer?.user.userPersonalData?.legalGuardian?.name ? (
+
{volunteer.user.userPersonalData?.legalGuardian?.name}
) : (
)}
{t('identification')}
- {volunteer?.legalRepresentative?.series ? (
-
{volunteer.legalRepresentative.series}
+ {volunteer?.user.userPersonalData?.legalGuardian?.identityDocumentSeries ? (
+
{volunteer.user.userPersonalData?.legalGuardian?.identityDocumentSeries}
) : (
)}
,
- {volunteer?.legalRepresentative?.no ? (
-
{volunteer?.legalRepresentative?.no}
+ {volunteer?.user.userPersonalData?.legalGuardian?.identityDocumentNumber ? (
+
{volunteer?.user.userPersonalData?.legalGuardian?.identityDocumentNumber}
) : (
)}
@@ -105,8 +89,8 @@ export const Signatures = ({ volunteer }: SignatureProps) => {
{t('tel_no')}
- {volunteer?.legalRepresentative?.tel ? (
-
{volunteer.legalRepresentative.tel}
+ {volunteer?.user.userPersonalData?.legalGuardian?.phone ? (
+
{volunteer?.user.userPersonalData?.legalGuardian?.phone}
) : (
)}
diff --git a/frontend/src/components/Stepper.tsx b/frontend/src/components/Stepper.tsx
index d4b4814cb..e4d776a5d 100644
--- a/frontend/src/components/Stepper.tsx
+++ b/frontend/src/components/Stepper.tsx
@@ -2,50 +2,50 @@ import React from 'react';
import { CheckIcon } from '@heroicons/react/24/outline';
interface Step {
- id: string;
- label: string;
+ id: string;
+ label: string;
}
interface StepperProps {
- steps: Step[];
- currentStep: number;
- completedSteps: boolean[];
- goToStep: (step: number) => void;
+ steps: Step[];
+ currentStep: number;
+ completedSteps: boolean[];
+ goToStep: (step: number) => void;
}
-const activeStyles = 'border-yellow text-yellow';
-const completedStyles = 'border-yellow bg-yellow';
+const activeStyles = 'border-yellow-500 text-yellow-500';
+const completedStyles = 'border-yellow-500 bg-yellow-500';
export const Stepper = ({ steps, currentStep, completedSteps, goToStep }: StepperProps) => {
- return (
-
- {steps.map((step, index) => {
- const isActive = index === currentStep;
- const isCompleted = completedSteps[index];
+ 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}
-
-
- );
- })}
-
- );
+ 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/containers/query/DocumentTemplatesTableWithQueryParams.tsx b/frontend/src/containers/query/DocumentTemplatesTableWithQueryParams.tsx
new file mode 100644
index 000000000..25e6eda42
--- /dev/null
+++ b/frontend/src/containers/query/DocumentTemplatesTableWithQueryParams.tsx
@@ -0,0 +1,40 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import React from 'react';
+import { QueryParams } from 'use-query-params';
+import {
+ getPaginationQueryParams,
+ IPaginationQueryParams,
+} from '../../common/constants/pagination';
+import { IHOCQueryProps } from '../../common/interfaces/hoc-query-props.interface';
+import { DocumentTemplateTable } from '../../components/DocumentTemplateTable';
+import { IDocumentTemplateListItem } from '../../common/interfaces/template.interface';
+
+export interface DocumentTemplatesQueryProps extends IPaginationQueryParams {
+
+}
+
+export type DocumentTemplatesProps = IHOCQueryProps
& {
+ selectedTemplate: IDocumentTemplateListItem | null;
+ onSelectTemplate: (template: IDocumentTemplateListItem) => void;
+}
+
+// set page default params
+const DEFAULT_QUERY_PARAMS = getPaginationQueryParams({ orderBy: 'name' });
+
+// set defaults (if needed) for other specific filter params
+const DocumentTemplatesTableWithQueryParams = ({ selectedTemplate, onSelectTemplate }: { selectedTemplate: IDocumentTemplateListItem | null, onSelectTemplate: (template: IDocumentTemplateListItem) => void }) => {
+ // set query config
+ const queryConfig = {
+ ...DEFAULT_QUERY_PARAMS,
+ };
+
+ return (
+
+ {(props: any) => {
+ return ;
+ }}
+
+ );
+};
+
+export default DocumentTemplatesTableWithQueryParams;
diff --git a/frontend/src/containers/query/DocumentVolunteersTableWithQueryParams.tsx b/frontend/src/containers/query/DocumentVolunteersTableWithQueryParams.tsx
new file mode 100644
index 000000000..6bd0c5a2e
--- /dev/null
+++ b/frontend/src/containers/query/DocumentVolunteersTableWithQueryParams.tsx
@@ -0,0 +1,60 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import React from 'react';
+import { StringParam, QueryParams, DateParam, ArrayParam, withDefault } from 'use-query-params';
+import {
+ getPaginationQueryParams,
+ IPaginationQueryParams,
+} from '../../common/constants/pagination';
+import { VolunteerStatus } from '../../common/enums/volunteer-status.enum';
+import { IHOCQueryProps } from '../../common/interfaces/hoc-query-props.interface';
+import DocumentVolunteersTable from '../../components/DocumentVolunteersTable';
+import { IVolunteer } from '../../common/interfaces/volunteer.interface';
+
+export interface VolunteersQueryProps extends IPaginationQueryParams {
+ volunteerStatus?: string;
+ search?: string;
+ branch?: string;
+ department?: string;
+ role?: string;
+ createdOnStart?: Date;
+ createdOnEnd?: Date;
+ age?: string;
+ location?: string[];
+}
+
+export type DocumentVolunteersProps = IHOCQueryProps & {
+ selectedVolunteers: IVolunteer[];
+ setSelectedVolunteers: (volunteers: IVolunteer[]) => void;
+}
+
+// set page default params
+const DEFAULT_QUERY_PARAMS = getPaginationQueryParams({ orderBy: 'user.name' });
+
+// set defaults (if needed) for other specific filter params
+const DocumentVolunteersTableWithQueryParams = ({ selectedVolunteers, setSelectedVolunteers }: { selectedVolunteers: IVolunteer[], setSelectedVolunteers: (volunteers: IVolunteer[]) => void }) => {
+ // set volunteer status default
+ const VolunteerStatusParam = withDefault(StringParam, VolunteerStatus.ACTIVE);
+ // set query config
+ const queryConfig = {
+ ...DEFAULT_QUERY_PARAMS,
+ volunteerStatus: VolunteerStatusParam,
+ search: StringParam,
+ branch: StringParam,
+ department: StringParam,
+ role: StringParam,
+ createdOnStart: DateParam,
+ createdOnEnd: DateParam,
+ age: StringParam,
+ location: ArrayParam,
+ };
+
+ return (
+
+ {(props: any) => {
+ return ;
+ }}
+
+ );
+};
+
+export default DocumentVolunteersTableWithQueryParams;
diff --git a/frontend/src/pages/GenerateContract.tsx b/frontend/src/pages/GenerateContract.tsx
index e9261fc87..05833f4e0 100644
--- a/frontend/src/pages/GenerateContract.tsx
+++ b/frontend/src/pages/GenerateContract.tsx
@@ -5,109 +5,18 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Stepper } from '../components/Stepper';
import Button from '../components/Button';
-import CardBody from '../components/CardBody';
-import DataTableComponent from '../components/DataTableComponent';
-import { ContractTemplate } from './ContractTemplates';
-import Card from '../layouts/CardLayout';
-import CardHeader from '../components/CardHeader';
-
-const ContractTemplatesTableHeader = [
- {
- id: 'name',
- name: 'Nume',
- sortable: true,
- grow: 4,
- minWidth: '9rem',
- selector: (row: ContractTemplate) => row.name,
- },
- {
- id: 'uses',
- name: 'Utilizări',
- sortable: true,
- grow: 1,
- minWidth: '5rem',
- // todo: get uses count
- selector: () => 'TODO',
- },
- {
- id: 'last_used',
- name: 'Ultima utilizare',
- sortable: true,
- grow: 1,
- minWidth: '5rem',
- // todo: get last usage count
- selector: () => 'TODO',
- },
- {
- id: 'created_by',
- name: 'Creat de',
- sortable: true,
- grow: 1,
- minWidth: '5rem',
- selector: (row: ContractTemplate) => row.createdByAdmin.name,
- },
- {
- id: 'created_at',
- name: 'Data creării',
- sortable: true,
- grow: 1,
- minWidth: '5rem',
- // todo: get created_at date
- selector: () => 'TODO',
- },
-];
-const templates: ContractTemplate[] = [
- {
- id: '9a827436-e0b8-4763-8b04-ccff3f9b2757',
- name: 'Test Template Lucia',
- organizationData: {
- CUI: '1278133',
- officialName: 'Tenebru Diamonds Industry',
- registeredOffice: 'Strada Cazarmii NR. 3392',
- legalRepresentativeName: 'John Dave',
- legalRepresentativeRole: 'Admin',
- },
- documentTerms: 'Contract terms
',
- createdByAdmin: {
- id: '8f2a561d-982f-465f-8dfb-bb2e16c39be6',
- name: 'Galdo Gerald',
- },
- },
- {
- id: 'b3c45678-d9e0-4f12-a3b4-56c7d8e9f012',
- name: 'Standard Contract Template',
- organizationData: {
- CUI: '9876543',
- officialName: 'Global Solutions Inc.',
- registeredOffice: 'Main Street 123',
- legalRepresentativeName: 'Jane Smith',
- legalRepresentativeRole: 'CEO',
- },
- documentTerms: 'Standard contract terms and conditions
',
- createdByAdmin: {
- id: '1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p',
- name: 'Admin User',
- },
- },
- {
- id: 'c7d89012-e3f4-5g6h-i7j8-9k0l1m2n3o4p',
- name: 'Volunteer Agreement Template',
- organizationData: {
- CUI: '5432109',
- officialName: 'Community Helpers Association',
- registeredOffice: 'Volunteer Avenue 456',
- legalRepresentativeName: 'Mark Johnson',
- legalRepresentativeRole: 'Director',
- },
- documentTerms: 'Volunteer agreement terms
',
- createdByAdmin: {
- id: 'q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6',
- name: 'Sarah Admin',
- },
- },
-];
+import DocumentVolunteersTableWithQueryParams from '../containers/query/DocumentVolunteersTableWithQueryParams';
+import { IDocumentTemplateListItem } from '../common/interfaces/template.interface';
+import { IVolunteer } from '../common/interfaces/volunteer.interface';
+import DocumentTemplatesTableWithQueryParams from '../containers/query/DocumentTemplatesTableWithQueryParams';
+import { DocumentContractFillCards } from '../components/DocumentContractFillCards';
+// import { ContractTemplate } from './ContractTemplates';
+// import { IVolunteer } from '../common/interfaces/volunteer.interface';
export const GenerateContract = () => {
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const [selectedVolunteers, setSelectedVolunteers] = useState([]);
+
const { t } = useTranslation(['volunteering_contracts', 'stepper']);
const navigate = useNavigate();
@@ -147,40 +56,25 @@ export const GenerateContract = () => {
setCurrentStep(currentStep - 1);
}
};
- const onSelectTemplate = (template: ContractTemplate[]) => {
- console.log(template);
+
+ const onSelectTemplate = (template: IDocumentTemplateListItem | null) => {
+ setSelectedTemplate(template);
+ };
+
+ const onSelectVolunteers = (volunteers: IVolunteer[]) => {
+ setSelectedVolunteers(volunteers);
};
const renderStep = () => {
switch (currentStep) {
case 0:
return (
-
-
-