diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bc25710439..77158ec56a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "@stablelib/base64": "^1.0.1", "@stripe/react-stripe-js": "^1.15.0", "@stripe/stripe-js": "^1.44.1", + "@types/pdfmake": "^0.2.12", "@types/stopword": "^2.0.1", "axios": "^1.8.2", "broadcast-channel": "^4.13.0", @@ -54,6 +55,7 @@ "p-queue": "^7.2.0", "papaparse": "^5.4.1", "pdfjs-dist": "^4.8.69", + "pdfmake": "^0.2.20", "perfect-freehand": "^1.2.2", "polyfill-object.fromentries": "^1.0.1", "react": "^18.3.0", @@ -77,7 +79,6 @@ "react-swipeable": "^7.0.0", "react-table": "^7.7.0", "react-textarea-autosize": "^8.3.3", - "react-to-print": "^3.1.1", "react-use": "^17.3.1", "react-use-scrollspy": "^3.0.2", "react-virtuoso": "^2.14.0", @@ -2499,6 +2500,57 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" }, + "node_modules/@foliojs-fork/fontkit": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.2.tgz", + "integrity": "sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA==", + "license": "MIT", + "dependencies": { + "@foliojs-fork/restructure": "^2.0.2", + "brotli": "^1.2.0", + "clone": "^1.0.4", + "deep-equal": "^1.0.0", + "dfa": "^1.2.0", + "tiny-inflate": "^1.0.2", + "unicode-properties": "^1.2.2", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/@foliojs-fork/linebreak": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.2.tgz", + "integrity": "sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg==", + "license": "MIT", + "dependencies": { + "base64-js": "1.3.1", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/@foliojs-fork/linebreak/node_modules/base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "license": "MIT" + }, + "node_modules/@foliojs-fork/pdfkit": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@foliojs-fork/pdfkit/-/pdfkit-0.15.3.tgz", + "integrity": "sha512-Obc0Wmy3bm7BINFVvPhcl2rnSSK61DQrlHU8aXnAqDk9LCjWdUOPwhgD8Ywz5VtuFjRxmVOM/kQ/XLIBjDvltw==", + "license": "MIT", + "dependencies": { + "@foliojs-fork/fontkit": "^1.9.2", + "@foliojs-fork/linebreak": "^1.1.1", + "crypto-js": "^4.2.0", + "jpeg-exif": "^1.1.4", + "png-js": "^1.0.0" + } + }, + "node_modules/@foliojs-fork/restructure": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz", + "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", + "license": "MIT" + }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.11.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", @@ -5054,6 +5106,25 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "node_modules/@types/pdfkit": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.4.tgz", + "integrity": "sha512-odAmVuuguRxKh1X4pbMrJMp8ecwNqHRw6lweupvzK+wuyNmi6wzlUlGVZ9EqMvp3Bs2+L9Ty0sRlrvKL+gsQZg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pdfmake": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@types/pdfmake/-/pdfmake-0.2.12.tgz", + "integrity": "sha512-YV3oY+J1eSHVH22fPuevE2kQdWvrpkEgYyHacExxgr//ZPjCitAggnysGjx/2VgiH0lEAGQ+5R+wZUGbAXn1Qg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/pdfkit": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", @@ -6853,6 +6924,15 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browser-assert": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", @@ -8158,6 +8238,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -8471,6 +8560,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/crypto-random-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", @@ -8837,6 +8932,26 @@ "node": ">=6" } }, + "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==", + "license": "MIT", + "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-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -9017,6 +9132,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -10807,7 +10928,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" } @@ -11868,7 +11988,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" }, @@ -12091,7 +12210,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" @@ -12250,6 +12368,12 @@ "node": ">=10" } }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "license": "MIT" + }, "node_modules/js-cookie": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", @@ -14988,6 +15112,33 @@ "path2d": "^0.2.1" } }, + "node_modules/pdfmake": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.20.tgz", + "integrity": "sha512-bGbxbGFP5p8PWNT3Phsu1ZcRLnRfF6jmnuKTkgmt6i5PZzSdX6JaB+NeTz9q+aocfW8SE9GUjL3o/5GroBqGcQ==", + "license": "MIT", + "dependencies": { + "@foliojs-fork/linebreak": "^1.1.2", + "@foliojs-fork/pdfkit": "^0.15.3", + "iconv-lite": "^0.6.3", + "xmldoc": "^2.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pdfmake/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/perfect-freehand": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.2.tgz", @@ -15081,6 +15232,11 @@ "node": ">=4" } }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "node_modules/polished": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz", @@ -16259,15 +16415,6 @@ "react": "^16.8.0 || ^17.0.0" } }, - "node_modules/react-to-print": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.1.1.tgz", - "integrity": "sha512-N0MUMhpl8nkGri13BjP7zusj3B/j+1eMOTt8N8PYuhBYGzA4PqTXqcihJ9cZw996dvhV6mBdwafIQCg3Ap5bKg==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19" - } - }, "node_modules/react-universal-interface": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", @@ -16455,7 +16602,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", @@ -16872,8 +17018,13 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "license": "BlueOak-1.0.0" }, "node_modules/scheduler": { "version": "0.23.1", @@ -17019,7 +17170,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", @@ -17820,6 +17970,12 @@ "resolved": "https://registry.npmjs.org/timezone-mock/-/timezone-mock-1.3.6.tgz", "integrity": "sha512-YcloWmZfLD9Li5m2VcobkCDNVaLMx8ohAb/97l/wYS3m+0TIEK5PFNMZZfRcusc6sFjIfxu8qcJT0CNnOdpqmg==" }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -18314,6 +18470,32 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", @@ -19721,6 +19903,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xmldoc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-2.0.2.tgz", + "integrity": "sha512-UiRwoSStEXS3R+YE8OqYv3jebza8cBBAI2y8g3B15XFkn3SbEOyyLnmPHjLBPZANrPJKEzxxB7A3XwcLikQVlQ==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -21475,6 +21669,54 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" }, + "@foliojs-fork/fontkit": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.2.tgz", + "integrity": "sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA==", + "requires": { + "@foliojs-fork/restructure": "^2.0.2", + "brotli": "^1.2.0", + "clone": "^1.0.4", + "deep-equal": "^1.0.0", + "dfa": "^1.2.0", + "tiny-inflate": "^1.0.2", + "unicode-properties": "^1.2.2", + "unicode-trie": "^2.0.0" + } + }, + "@foliojs-fork/linebreak": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.2.tgz", + "integrity": "sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg==", + "requires": { + "base64-js": "1.3.1", + "unicode-trie": "^2.0.0" + }, + "dependencies": { + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + } + } + }, + "@foliojs-fork/pdfkit": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@foliojs-fork/pdfkit/-/pdfkit-0.15.3.tgz", + "integrity": "sha512-Obc0Wmy3bm7BINFVvPhcl2rnSSK61DQrlHU8aXnAqDk9LCjWdUOPwhgD8Ywz5VtuFjRxmVOM/kQ/XLIBjDvltw==", + "requires": { + "@foliojs-fork/fontkit": "^1.9.2", + "@foliojs-fork/linebreak": "^1.1.1", + "crypto-js": "^4.2.0", + "jpeg-exif": "^1.1.4", + "png-js": "^1.0.0" + } + }, + "@foliojs-fork/restructure": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz", + "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==" + }, "@formatjs/ecma402-abstract": { "version": "1.11.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", @@ -23204,6 +23446,23 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "@types/pdfkit": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.4.tgz", + "integrity": "sha512-odAmVuuguRxKh1X4pbMrJMp8ecwNqHRw6lweupvzK+wuyNmi6wzlUlGVZ9EqMvp3Bs2+L9Ty0sRlrvKL+gsQZg==", + "requires": { + "@types/node": "*" + } + }, + "@types/pdfmake": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@types/pdfmake/-/pdfmake-0.2.12.tgz", + "integrity": "sha512-YV3oY+J1eSHVH22fPuevE2kQdWvrpkEgYyHacExxgr//ZPjCitAggnysGjx/2VgiH0lEAGQ+5R+wZUGbAXn1Qg==", + "requires": { + "@types/node": "*", + "@types/pdfkit": "*" + } + }, "@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", @@ -24479,6 +24738,14 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, + "brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "requires": { + "base64-js": "^1.1.2" + } + }, "browser-assert": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", @@ -25252,6 +25519,11 @@ } } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -25511,6 +25783,11 @@ "randomfill": "^1.0.4" } }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "crypto-random-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", @@ -25808,6 +26085,19 @@ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true }, + "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==", + "requires": { + "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" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -25935,6 +26225,11 @@ "dequal": "^2.0.0" } }, + "dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -27243,8 +27538,7 @@ "functions-have-names": { "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 + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" }, "fuzzysort": { "version": "3.1.0", @@ -27976,7 +28270,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, "requires": { "has-tostringtag": "^1.0.0" } @@ -28106,7 +28399,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, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -28207,6 +28499,11 @@ "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==" }, + "jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==" + }, "js-cookie": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", @@ -30100,6 +30397,27 @@ "path2d": "^0.2.1" } }, + "pdfmake": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.20.tgz", + "integrity": "sha512-bGbxbGFP5p8PWNT3Phsu1ZcRLnRfF6jmnuKTkgmt6i5PZzSdX6JaB+NeTz9q+aocfW8SE9GUjL3o/5GroBqGcQ==", + "requires": { + "@foliojs-fork/linebreak": "^1.1.2", + "@foliojs-fork/pdfkit": "^0.15.3", + "iconv-lite": "^0.6.3", + "xmldoc": "^2.0.1" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "perfect-freehand": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.2.tgz", @@ -30169,6 +30487,11 @@ } } }, + "png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "polished": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz", @@ -30988,11 +31311,6 @@ "use-latest": "^1.0.0" } }, - "react-to-print": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.1.1.tgz", - "integrity": "sha512-N0MUMhpl8nkGri13BjP7zusj3B/j+1eMOTt8N8PYuhBYGzA4PqTXqcihJ9cZw996dvhV6mBdwafIQCg3Ap5bKg==" - }, "react-universal-interface": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", @@ -31142,7 +31460,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, "requires": { "call-bind": "^1.0.6", "define-properties": "^1.2.1", @@ -31437,8 +31754,12 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==" }, "scheduler": { "version": "0.23.1", @@ -31557,7 +31878,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, "requires": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -32130,6 +32450,11 @@ "resolved": "https://registry.npmjs.org/timezone-mock/-/timezone-mock-1.3.6.tgz", "integrity": "sha512-YcloWmZfLD9Li5m2VcobkCDNVaLMx8ohAb/97l/wYS3m+0TIEK5PFNMZZfRcusc6sFjIfxu8qcJT0CNnOdpqmg==" }, + "tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, "tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -32479,6 +32804,31 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, + "unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "requires": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + }, + "dependencies": { + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + } + } + }, "unified": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", @@ -33287,6 +33637,14 @@ "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", "dev": true }, + "xmldoc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-2.0.2.tgz", + "integrity": "sha512-UiRwoSStEXS3R+YE8OqYv3jebza8cBBAI2y8g3B15XFkn3SbEOyyLnmPHjLBPZANrPJKEzxxB7A3XwcLikQVlQ==", + "requires": { + "sax": "^1.2.4" + } + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index c33e405836..f8fb917f37 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@stablelib/base64": "^1.0.1", "@stripe/react-stripe-js": "^1.15.0", "@stripe/stripe-js": "^1.44.1", + "@types/pdfmake": "^0.2.12", "@types/stopword": "^2.0.1", "axios": "^1.8.2", "broadcast-channel": "^4.13.0", @@ -50,6 +51,7 @@ "p-queue": "^7.2.0", "papaparse": "^5.4.1", "pdfjs-dist": "^4.8.69", + "pdfmake": "^0.2.20", "perfect-freehand": "^1.2.2", "polyfill-object.fromentries": "^1.0.1", "react": "^18.3.0", @@ -73,7 +75,6 @@ "react-swipeable": "^7.0.0", "react-table": "^7.7.0", "react-textarea-autosize": "^8.3.3", - "react-to-print": "^3.1.1", "react-use": "^17.3.1", "react-use-scrollspy": "^3.0.2", "react-virtuoso": "^2.14.0", diff --git a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponseNavbar.tsx b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponseNavbar.tsx index f84c855d05..a04e0e2371 100644 --- a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponseNavbar.tsx +++ b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponseNavbar.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { BiChevronLeft, BiChevronRight, BiLeftArrowAlt } from 'react-icons/bi' import { FaRegFilePdf } from 'react-icons/fa6' @@ -8,7 +8,6 @@ import { useNavigate, useParams, } from 'react-router-dom' -import { useReactToPrint } from 'react-to-print' import { Box, ButtonGroup, @@ -33,7 +32,7 @@ import { useUser } from '~features/user/queries' import { useUnlockedResponses } from '../ResponsesPage/storage/UnlockedResponses/UnlockedResponsesProvider' -import PrintableResponse from './PrintableResponse' +import generatePdf from './utils/generatePdf' import { useIndividualSubmission } from './queries' export const IndividualResponseNavbar = (): JSX.Element => { @@ -56,7 +55,10 @@ export const IndividualResponseNavbar = (): JSX.Element => { isAnyFetching, } = useUnlockedResponses() const { data: form, isLoading: isFormLoading } = useAdminForm() - const { isLoading } = useIndividualSubmission() + const { data: submission, isLoading: isResponsesLoading } = + useIndividualSubmission() + + const isLoading = isFormLoading || isResponsesLoading const nextSubmissionId = useMemo( () => getNextSubmissionId(submissionId), @@ -130,12 +132,6 @@ export const IndividualResponseNavbar = (): JSX.Element => { const isAdminPrintPdfEnabled = useFeatureIsOn(featureFlags.adminPrintPdf) - const printableResponseRef = useRef(null) - const reactToPrintFn = useReactToPrint({ - contentRef: printableResponseRef, - documentTitle: `${form ? `${form.title}_formId_${form._id}_` : ''}submissionId_${submissionId}_response.pdf`, - }) - return ( { } - isLoading={isLoading || isFormLoading} + isLoading={isLoading} onClick={() => { - datadogLogs.logger.info( - `IndividualResponseNavbar: admin printing pdf`, - { - meta: { - action: 'adminPrintPdf', - userId: user?._id, - submissionId: submissionId, + if (form && submission) { + datadogLogs.logger.info( + `IndividualResponseNavbar: admin printing pdf`, + { + meta: { + action: 'adminPrintPdf', + userId: user?._id, + submissionId: submissionId, + }, }, - }, - ) - reactToPrintFn() + ) + generatePdf({ + formId: form._id, + formTitle: form.title, + submissionId, + responses: submission.responses, + submissionTime: submission.submissionTime, + }) + } }} variant="clear" /> - )} diff --git a/frontend/src/features/admin-form/responses/IndividualResponsePage/PrintableResponse.stories.tsx b/frontend/src/features/admin-form/responses/IndividualResponsePage/PrintableResponse.stories.tsx deleted file mode 100644 index e8f453e142..0000000000 --- a/frontend/src/features/admin-form/responses/IndividualResponsePage/PrintableResponse.stories.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import { Meta, StoryFn } from '@storybook/react' - -import { BasicField } from '~shared/types/field' - -import { AugmentedDecryptedResponse } from '../ResponsesPage/storage/utils/augmentDecryptedResponses' - -import { PrintableResponse } from './PrintableResponse' - -export default { - title: 'Features/AdminForm/Responses/PrintableResponse', - component: PrintableResponse, - parameters: { - layout: 'fullscreen', - chromatic: { pauseAnimationAtEnd: true }, - }, -} as Meta - -const SAMPLE_SIGNATURE_ANSWER_ARRAY = [ - 'draw', - '[[[287.328125,44.2421875,0.5],[287.328125,44.47265625,0.5],[287.328125,47.4453125,0.5],[280.58203125,62.3359375,0.5],[274.9453125,72.90234375,0.5],[273.5,77.2265625,0.5],[267.86328125,87.79296875,0.5],[262.9296875,99.0625,0.5],[257.29296875,110.33203125,0.5],[253.40234375,119.40625,0.5],[252.60546875,121.78515625,0.5],[252.234375,123.62890625,0.5],[250.30859375,127.953125,0.5],[249.45703125,130.92578125,0.5],[249.0859375,132.76953125,0.5],[249.0859375,133.33984375,0.5],[249.0859375,133.59765625,0.5]],[[287.78515625,79.55078125,0.5],[287.31640625,79.55078125,0.5],[286.375,79.55078125,0.5],[285.0078125,79.55078125,0.5],[282.625,79.55078125,0.5],[278.875,81.078125,0.5],[277.02734375,82.18359375,0.5],[276.453125,82.18359375,0.5],[275.51171875,82.49609375,0.5],[274.14453125,82.8359375,0.5],[273.8828125,82.8359375,0.5],[273.30859375,82.8359375,0.5],[273.046875,82.8359375,0.5],[272.57421875,82.8359375,0.5],[271.73828125,83.37890625,0.5],[271.21484375,83.63671875,0.5],[270.953125,83.89453125,0.5],[270.69140625,83.89453125,0.5],[270.69140625,84.13671875,0.5],[270.69140625,84.3671875,0.5],[270.69140625,84.59765625,0.5],[270.92578125,84.83203125,0.5],[271.18359375,85.08984375,0.5],[272.37890625,85.97265625,0.5],[273.484375,87.44921875,0.5],[275.18359375,89.99609375,0.5],[276.9921875,93.1640625,0.5],[279.25390625,96.33203125,0.5],[281.0625,99.5,0.5],[281.7421875,100.86328125,0.5],[284.65625,105.875,0.5],[285.39453125,107.71875,0.5],[286.07421875,109.08203125,0.5],[286.64453125,109.65234375,0.5],[287.26953125,110.58984375,0.5],[287.52734375,110.84765625,0.5],[287.78515625,111.10546875,0.5],[288.04296875,111.36328125,0.5],[288.04296875,111.62109375,0.5],[288.30078125,111.62109375,0.5],[288.30078125,111.87890625,0.5],[288.55859375,111.87890625,0.5],[288.55859375,112.13671875,0.5],[288.81640625,112.39453125,0.5],[288.81640625,112.65234375,0.5],[289.07421875,112.91015625,0.5],[289.33203125,113.16796875,0.5],[289.33203125,113.41015625,0.5],[289.58984375,113.41015625,0.5],[289.82421875,113.41015625,0.5]],[[297.88671875,94.546875,0.5],[297.88671875,94.78515625,0.5],[298.1171875,94.78515625,0.5],[299.0546875,94.78515625,0.5],[300.41796875,94.78515625,0.5],[301.78125,94.78515625,0.5],[303.14453125,94.78515625,0.5],[304.98828125,94.78515625,0.5],[307.3671875,94.78515625,0.5],[309.78125,94.78515625,0.5],[310.71875,94.78515625,0.5],[312.08203125,94.78515625,0.5],[312.65234375,94.78515625,0.5],[313.22265625,94.78515625,0.5],[313.48046875,94.78515625,0.5],[313.48046875,94.55078125,0.5],[313.48046875,93.9765625,0.5],[312.90625,93.40234375,0.5],[311.703125,91.88671875,0.5],[311.01953125,90.51953125,0.5],[309.36328125,88.55078125,0.5],[307.73828125,86.5546875,0.5],[307.109375,85.61328125,0.5],[306.53515625,85.0390625,0.5],[305.9609375,84.75,0.5],[305.01953125,84.43359375,0.5],[304.4453125,84.14453125,0.5],[303.50390625,83.828125,0.5],[302.5625,83.828125,0.5],[301.62109375,83.828125,0.5],[299.73828125,83.828125,0.5],[299.1640625,83.828125,0.5],[298.58984375,83.828125,0.5],[298.015625,83.828125,0.5],[297.44140625,83.828125,0.5],[297.1796875,83.828125,0.5],[296.60546875,83.828125,0.5],[296.34375,83.828125,0.5],[295.76953125,83.828125,0.5],[295.5078125,83.828125,0.5],[294.93359375,84.11328125,0.5],[294.359375,84.3984375,0.5],[293.78515625,84.68359375,0.5],[293.2109375,84.96875,0.5],[292.63671875,85.5390625,0.5],[292.375,85.796875,0.5],[291.80078125,86.3671875,0.5],[291.5390625,86.625,0.5],[291.25,87.1953125,0.5],[291.25,87.765625,0.5],[290.98828125,88.59375,0.5],[290.98828125,89.1640625,0.5],[290.98828125,89.421875,0.5],[290.98828125,89.9921875,0.5],[290.98828125,90.25,0.5],[290.98828125,90.5078125,0.5],[290.98828125,91.078125,0.5],[290.98828125,91.3359375,0.5],[290.98828125,91.90625,0.5],[290.98828125,92.84375,0.5],[290.98828125,93.78125,0.5],[291.30078125,94.71875,0.5],[291.640625,96.08203125,0.5],[291.98046875,97.4453125,0.5],[292.3203125,98.80859375,0.5],[292.66015625,100.171875,0.5],[293.28515625,101.109375,0.5],[293.625,102.47265625,0.5],[294.5078125,103.98046875,0.5],[295.078125,104.265625,0.5],[295.36328125,104.8359375,0.5],[295.93359375,105.12109375,0.5],[296.19140625,105.37890625,0.5],[296.76171875,105.37890625,0.5],[297.01953125,105.63671875,0.5],[297.58984375,105.63671875,0.5],[298.16015625,105.63671875,0.5],[299.09765625,105.63671875,0.5],[300.03515625,105.63671875,0.5],[300.97265625,105.63671875,0.5],[301.91015625,105.63671875,0.5],[302.84765625,105.63671875,0.5],[304.2109375,105.63671875,0.5],[305.57421875,105.63671875,0.5],[306.9375,105.63671875,0.5],[308.30078125,105.63671875,0.5],[309.6640625,105.63671875,0.5],[311.02734375,105.63671875,0.5],[312.9609375,105.63671875,0.5],[314.15625,105.63671875,0.5],[314.4140625,105.63671875,0.5],[314.671875,105.63671875,0.5],[314.9296875,105.63671875,0.5]],[[320.58203125,90.640625,0.5],[320.58203125,90.87109375,0.5],[320.58203125,92.234375,0.5],[321.26171875,93.85546875,0.5],[321.57421875,94.79296875,0.5],[321.88671875,95.73046875,0.5],[321.88671875,95.98828125,0.5],[321.88671875,96.55859375,0.5],[322.171875,97.12890625,0.5],[322.171875,97.69921875,0.5],[322.796875,98.63671875,0.5],[323.734375,100.51171875,0.5],[324.47265625,102.35546875,0.5],[325.890625,105.13671875,0.5],[327.4765625,107.12109375,0.5],[328.15625,108.484375,0.5],[328.89453125,110.328125,0.5],[329.51953125,111.265625,0.5],[330.14453125,112.203125,0.5],[330.4296875,112.7734375,0.5],[330.71484375,113.34375,0.5],[330.97265625,113.6015625,0.5],[331.23046875,113.859375,0.5],[331.48828125,113.859375,0.5],[331.73046875,113.859375,0.5],[331.97265625,113.859375,0.5],[332.21484375,113.61328125,0.5],[332.83984375,112.671875,0.5],[333.9453125,111.19140625,0.5],[335.05078125,109.34375,0.5],[336.75,106.79296875,0.5],[338.55859375,103.62109375,0.5],[340.8203125,100.44921875,0.5],[342.62890625,96.82421875,0.5],[343.30859375,95.45703125,0.5],[345.1171875,92.28515625,0.5],[346.22265625,90.4375,0.5],[347.328125,88.58984375,0.5],[348.0078125,87.22265625,0.5],[348.3203125,86.28125,0.5],[348.60546875,85.70703125,0.5],[348.86328125,85.4453125,0.5]],[[366.32421875,89.3125,0.5],[366.32421875,89.77734375,0.5],[366.32421875,90.71484375,0.5],[366.32421875,92.078125,0.5],[366.32421875,92.6484375,0.5],[366.32421875,93.5859375,0.5],[366.32421875,94.5234375,0.5],[366.32421875,95.88671875,0.5],[366.32421875,97.02734375,0.5],[366.32421875,97.54296875,0.5],[366.32421875,98.05859375,0.5],[366.32421875,98.31640625,0.5],[366.32421875,99.6796875,0.5],[366.32421875,100.6171875,0.5],[366.32421875,101.5546875,0.5],[366.32421875,102.91796875,0.5],[366.32421875,103.85546875,0.5],[366.32421875,105.21875,0.5],[366.32421875,105.7890625,0.5],[366.32421875,106.7265625,0.5],[366.32421875,107.296875,0.5],[366.32421875,107.5546875,0.5],[366.32421875,107.8125,0.5],[366.32421875,108.0703125,0.5],[366.32421875,108.30078125,0.5],[366.32421875,108.55859375,0.5],[366.32421875,108.81640625,0.5],[366.32421875,109.38671875,0.5],[365.75,110.52734375,0.5],[365.75,111.09765625,0.5],[365.75,111.35546875,0.5],[365.75,111.61328125,0.5],[365.75,111.87109375,0.5]],[[365.75,72.12890625,0.5]],[[383.84765625,81.10546875,0.5],[383.84765625,81.3359375,0.5],[383.84765625,82.69921875,0.5],[383.84765625,86.921875,0.5],[383.84765625,89.89453125,0.5],[383.84765625,92.2734375,0.5],[383.84765625,92.84375,0.5],[383.84765625,94.20703125,0.5],[383.84765625,95.5703125,0.5],[383.84765625,96.140625,0.5],[383.84765625,96.3984375,0.5],[383.84765625,96.65625,0.5],[383.84765625,96.88671875,0.5],[383.84765625,98.76171875,0.5],[383.84765625,100.125,0.5],[383.84765625,101.0625,0.5],[383.84765625,101.6328125,0.5],[383.84765625,102.203125,0.5],[383.84765625,102.7734375,0.5],[383.84765625,103.03125,0.5],[383.84765625,103.2890625,0.5],[383.84765625,103.0546875,0.5],[383.84765625,102.80859375,0.5],[383.84765625,102.28515625,0.5],[383.84765625,101.44921875,0.5],[383.84765625,100.08203125,0.5],[384.609375,95.8515625,0.5],[385.00390625,93.46875,0.5],[385.3984375,91.0859375,0.5],[385.765625,89.23828125,0.5],[386.16015625,86.85546875,0.5],[387.265625,83.16015625,0.5],[388.28515625,80.42578125,0.5],[388.5703125,79.8515625,0.5],[388.85546875,79.27734375,0.5],[389.42578125,78.98828125,0.5],[389.99609375,78.4140625,0.5],[390.56640625,78.4140625,0.5],[391.13671875,78.125,0.5],[392.07421875,78.125,0.5],[393.01171875,77.80859375,0.5],[394.375,77.80859375,0.5],[395.3125,77.80859375,0.5],[396.25,77.80859375,0.5],[397.1875,77.80859375,0.5],[397.7578125,77.80859375,0.5],[398.6953125,77.80859375,0.5],[398.953125,77.80859375,0.5],[399.23828125,78.37890625,0.5],[399.55078125,79.31640625,0.5],[400.23046875,82.09765625,0.5],[400.23046875,84.4765625,0.5],[400.625,86.85546875,0.5],[400.625,88.21875,0.5],[401.01953125,90.59765625,0.5],[401.4140625,92.9765625,0.5],[401.80859375,95.35546875,0.5],[402.203125,97.734375,0.5],[403.55859375,101.35546875,0.5],[403.8984375,102.71875,0.5],[405.70703125,105.88671875,0.5],[407.29296875,107.87109375,0.5],[407.91796875,108.80859375,0.5],[409.50390625,110.79296875,0.5],[410.98046875,111.8984375,0.5],[412.45703125,113.00390625,0.5],[414.30078125,113.7421875,0.5],[415.6640625,114.08203125,0.5],[417.02734375,114.421875,0.5],[419.75390625,114.421875,0.5],[420.69140625,114.421875,0.5],[421.62890625,114.421875,0.5],[422.56640625,114.10546875,0.5],[423.58984375,113.078125,0.5],[424.61328125,112.05078125,0.5],[424.92578125,111.109375,0.5],[425.49609375,110.53515625,0.5],[425.78125,109.9609375,0.5],[426.0390625,109.69921875,0.5],[426.296875,109.4375,0.5],[426.5546875,109.17578125,0.5]],[[447.078125,79.3359375,0.5],[447.30859375,79.3359375,0.5],[449.15234375,79.3359375,0.5],[451.53125,79.3359375,0.5],[455.15234375,79.3359375,0.5],[458.125,79.3359375,0.5],[460.50390625,79.3359375,0.5],[461.07421875,79.3359375,0.5],[462.4375,79.3359375,0.5],[463.80078125,79.3359375,0.5],[464.37109375,79.3359375,0.5]],[[442.26171875,92.8359375,0.5],[443.22265625,92.8359375,0.5],[444.16015625,92.8359375,0.5],[447.1328125,92.8359375,0.5],[450.10546875,92.8359375,0.5],[454.015625,92.8359375,0.5],[456.98828125,92.8359375,0.5],[458.3515625,92.8359375,0.5],[459.2890625,92.8359375,0.5],[459.546875,92.8359375,0.5],[459.8046875,92.8359375,0.5]],[[471.69921875,54.9609375,0.5],[471.69921875,55.19921875,0.5],[471.9296875,55.19921875,0.5],[473.29296875,55.19921875,0.5],[475.13671875,55.19921875,0.5],[477.515625,55.19921875,0.5],[479.359375,55.56640625,0.5],[479.9296875,55.8515625,0.5],[480.953125,56.875,0.5],[481.890625,57.1875,0.5],[482.4609375,57.47265625,0.5],[482.71875,57.73046875,0.5],[482.71875,57.97265625,0.5],[482.71875,58.21484375,0.5],[482.71875,58.78515625,0.5],[482.71875,60.62890625,0.5],[482.71875,64.25,0.5],[482.71875,67.22265625,0.5],[482.71875,72.3046875,0.5],[482.71875,77.38671875,0.5],[482.71875,82.46875,0.5],[482.20703125,87.55078125,0.5],[480.6796875,92.6328125,0.5],[477.33984375,101.3359375,0.5],[476.23046875,103.1796875,0.5],[474.10546875,105.7265625,0.5],[472.40234375,108.2734375,0.5],[470.921875,109.75,0.5],[469.44140625,110.85546875,0.5],[468.4140625,111.87890625,0.5],[467.83984375,112.44921875,0.5],[467.265625,112.734375,0.5],[466.69140625,113.01953125,0.5],[466.4296875,113.27734375,0.5],[466.16796875,113.27734375,0.5],[466.16796875,113.53515625,0.5],[465.921875,113.53515625,0.5],[465.66015625,113.79296875,0.5],[465.3984375,114.05078125,0.5],[464.82421875,114.3359375,0.5],[464.30078125,114.8515625,0.5],[463.7265625,115.13671875,0.5],[463.46484375,115.39453125,0.5],[463.203125,115.65234375,0.5],[462.94140625,115.65234375,0.5],[462.94140625,115.91015625,0.5]]]', -] - -const mockResponses: AugmentedDecryptedResponse[] = [ - { - _id: 'section-1', - fieldType: BasicField.Section, - question: 'Personal Information', - answer: '', - }, - { - _id: 'name-1', - fieldType: BasicField.ShortText, - question: 'Full Name', - answer: 'John Doe', - }, - { - _id: 'email-1', - fieldType: BasicField.Email, - question: 'Email Address', - answer: 'john.doe@example.com', - }, - { - _id: 'mobile-1', - fieldType: BasicField.Mobile, - question: 'Mobile Number', - answer: '+65 9123 4567', - }, - { - _id: 'address-1', - fieldType: BasicField.Address, - question: 'Home Address', - answerArray: ['123 Main Street', 'Singapore', 'Singapore', '123456'], - }, - { - _id: 'date-1', - fieldType: BasicField.Date, - question: 'Date of Birth', - answer: '1990-01-15', - }, - { - _id: 'radio-1', - fieldType: BasicField.Radio, - question: 'Gender', - answer: 'Male', - }, - { - _id: 'checkbox-1', - fieldType: BasicField.Checkbox, - question: 'Interests', - answerArray: ['Technology', 'Sports', 'Music'], - }, - { - _id: 'dropdown-1', - fieldType: BasicField.Dropdown, - question: 'Country', - answer: 'Singapore', - }, - { - _id: 'rating-1', - fieldType: BasicField.Rating, - question: 'Satisfaction Rating', - answer: '4', - }, - { - _id: 'textarea-1', - fieldType: BasicField.LongText, - question: 'Comments', - answer: - 'This is a long text response that demonstrates how the printable response handles multi-line text content. It should wrap properly and maintain formatting.', - }, - { - _id: 'table-1', - fieldType: BasicField.Table, - question: 'Family Members', - answerArray: [ - ['Jane Doe', 'Spouse'], - ['Johnny Doe', 'Son'], - ['Janet Doe', 'Daughter'], - ], - }, - { - _id: 'number-1', - fieldType: BasicField.Number, - question: 'Age', - answer: '34', - }, - { - _id: 'decimal-1', - fieldType: BasicField.Decimal, - question: 'Height (cm)', - answer: '175.5', - }, - { - _id: 'nric-1', - fieldType: BasicField.Nric, - question: 'NRIC', - answer: 'S1234567A', - }, - { - _id: 'uen-1', - fieldType: BasicField.Uen, - question: 'UEN', - answer: '201234567M', - }, - { - _id: 'yesno-1', - fieldType: BasicField.YesNo, - question: 'Are you a Singapore citizen?', - answer: 'Yes', - }, - { - _id: 'signature-1', - fieldType: BasicField.Signature, - question: 'Digital Signature', - answerArray: SAMPLE_SIGNATURE_ANSWER_ARRAY, - }, -] - -const Template: StoryFn = (args) => ( - -) - -export const Default = Template.bind({}) -Default.args = { - formTitle: 'Default Form Submission', - formId: '61540ece3d4a6e50ac0cc6ff', - decryptedResponses: mockResponses, - responseId: '68e4a1b6aa7e70297d82e8e2', - submissionTime: 'Mon, 6 Oct 2025, 02:00:31 PM', -} - -export const MinimalForm = Template.bind({}) -MinimalForm.args = { - ...Default.args, - formTitle: 'Minimal Form Submission', - formId: '61540ece3d4a6e50ac0cc6ff', - decryptedResponses: [ - { - _id: 'name-1', - fieldType: BasicField.ShortText, - question: 'Name', - answer: 'Alice Smith', - }, - { - _id: 'email-1', - fieldType: BasicField.Email, - question: 'Email', - answer: 'alice@example.com', - }, - ], -} - -export const LongFormTitle = Template.bind({}) -LongFormTitle.args = { - ...Default.args, - formTitle: - 'Comprehensive Employee Onboarding and Information Collection Form for New Hires', - formId: '61540ece3d4a6e50ac0cc6ff', - decryptedResponses: mockResponses.slice(0, 5), -} - -export const EmptyResponses = Template.bind({}) -EmptyResponses.args = { - ...Default.args, - formTitle: 'Empty Form', - formId: '61540ece3d4a6e50ac0cc6ff', - decryptedResponses: [], -} - -export const OnlySections = Template.bind({}) -OnlySections.args = { - ...Default.args, - formTitle: 'Form with Headings Only', - formId: '61540ece3d4a6e50ac0cc6ff', - decryptedResponses: [ - { - _id: 'section-1', - fieldType: BasicField.Section, - question: 'Section 1', - answer: '', - }, - { - _id: 'section-2', - fieldType: BasicField.Section, - question: 'Section 2', - answer: '', - }, - ], -} - -export const TableHeavyForm = Template.bind({}) -TableHeavyForm.args = { - ...Default.args, - formTitle: 'Table Heavy Form', - formId: '61540ece3d4a6e50ac0cc6ff', - decryptedResponses: [ - { - _id: 'table-1', - fieldType: BasicField.Table, - question: 'Products', - answerArray: [ - ['Product A', '100', 'In Stock'], - ['Product B', '200', 'Out of Stock'], - ['Product C', '150', 'In Stock'], - ['Product D', '75', 'Low Stock'], - ['Product E', '300', 'In Stock'], - ], - }, - { - _id: 'table-2', - fieldType: BasicField.Table, - question: 'Employees', - answerArray: [ - ['John Smith', 'Manager', 'john@company.com'], - ['Jane Doe', 'Developer', 'jane@company.com'], - ['Bob Johnson', 'Designer', 'bob@company.com'], - ], - }, - ], -} - -export const LongTextForm = Template.bind({}) -LongTextForm.args = { - ...Default.args, - formTitle: 'Feedback Form', - formId: '61540ece3d4a6e50ac0cc6ff', - decryptedResponses: [ - { - _id: 'textarea-1', - fieldType: BasicField.LongText, - question: 'Detailed Feedback', - answer: - 'This is a very long response that demonstrates how the printable response component handles extensive text content. It should properly wrap and maintain readability even with substantial amounts of text. The component should handle line breaks and formatting appropriately to ensure the content remains legible when printed or viewed in different contexts.', - }, - { - _id: 'textarea-2', - fieldType: BasicField.LongText, - question: 'Additional Comments', - answer: - 'Another long text field with multiple paragraphs.\n\nThis demonstrates how the component handles multiple paragraphs and line breaks within a single response field. The formatting should be preserved and the content should be clearly readable.', - }, - ], -} - -export const OptionalSignature = Template.bind({}) -OptionalSignature.args = { - ...Default.args, - formTitle: 'Optional signature with and without answer', - formId: '61540ece3d4a6e50ac0cc6ff', - decryptedResponses: [ - { - _id: 'signature-with-answer', - fieldType: BasicField.Signature, - question: 'Signature with answer', - answerArray: SAMPLE_SIGNATURE_ANSWER_ARRAY, - }, - { - _id: 'signature-no-answer', - fieldType: BasicField.Signature, - question: 'Signature with no answer', - answerArray: [], - }, - { - _id: 'shorttext-1', - fieldType: BasicField.ShortText, - question: 'Short text question', - answer: 'Purpose is to assert empty signature row size', - }, - ], -} diff --git a/frontend/src/features/admin-form/responses/IndividualResponsePage/PrintableResponse.tsx b/frontend/src/features/admin-form/responses/IndividualResponsePage/PrintableResponse.tsx deleted file mode 100644 index 8d175b4341..0000000000 --- a/frontend/src/features/admin-form/responses/IndividualResponsePage/PrintableResponse.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { forwardRef } from 'react' -import { Box, Text } from '@chakra-ui/react' -import { FieldType } from '@opengovsg/formsg-sdk/dist/types' - -import { BasicField } from '~shared/types/field' -import { handleAddressResponseDisplay } from '~shared/utils/address' - -import { showOnlyWhenPrintCss } from '~utils/showOnlyWhenPrintCss' - -import { useAdminForm } from '~features/admin-form/common/queries' - -import { AugmentedDecryptedResponse } from '../ResponsesPage/storage/utils/augmentDecryptedResponses' - -import { useIndividualSubmission } from './queries' -import { SignatureCanvas } from './SignatureCanvas' - -const SIGNATURE_PDF_FIXED_WIDTH = 300 // Same as the backend template's signature width - -const TableRow = ({ children }: { children: React.ReactNode }) => ( - - {children} - -) - -const PaddingStyle = { - padding: '8px 4px', -} - -const TableFullItem = ({ children }: { children: React.ReactNode }) => ( - - {children} - -) - -const TableSingleColItem = ({ children }: { children: React.ReactNode }) => ( - - {children} - -) - -const StandardPrintableRow = ({ - question, - answer, -}: { - question: string - answer: string -}) => { - return ( - - {question} - {answer} - - ) -} - -const PrintableDecryptedRow = ({ - row, -}: { - row: AugmentedDecryptedResponse -}) => { - switch (row.fieldType as FieldType) { - case BasicField.Section: - return ( - - - - {row.question} - - - - ) - case BasicField.Address: { - const transformedAddress = handleAddressResponseDisplay( - row.answerArray as string[], - ).join(', ') - return ( - - ) - } - case BasicField.Table: - return ( - <> - {row.answerArray?.map((ans, idx) => ( - - ))} - - ) - case BasicField.Signature: - return ( - - {row.question} - - - - - ) - default: - return ( - - ) - } -} - -const getDefaultRows = ({ - responseId, - submissionTime, -}: { - responseId: string - submissionTime: string -}) => [ - { - question: 'Response ID', - answer: responseId, - }, - { - question: 'Time Submitted', - answer: submissionTime, - }, -] - -const PrintableResponseRows = ({ - decryptedResponses, - responseId, - submissionTime, -}: { - decryptedResponses: AugmentedDecryptedResponse[] - responseId: string - submissionTime: string -}) => { - return ( - - - {getDefaultRows({ responseId, submissionTime }).map((r, idx) => ( - - ))} - {decryptedResponses.map((r, idx) => ( - - ))} - -
- ) -} - -const isTest = import.meta.env.STORYBOOK_NODE_ENV === 'test' -const MOCK_CONSTANT_FORM_LINK = 'https://form.gov.sg/64e2f7ae841cbe0012e785e7' - -export const PrintableResponse = ({ - formTitle, - formId, - decryptedResponses, - responseId, - submissionTime, -}: { - formTitle: string - formId: string - decryptedResponses: AugmentedDecryptedResponse[] - responseId: string - submissionTime: string -}) => { - return ( - - - {formTitle} - - {/* RATIONALE: Prevent noisy diffs from being detected during storybook snapshot comparisons due to dynamic form link. */} - {isTest - ? MOCK_CONSTANT_FORM_LINK - : `${window.location.origin}/${formId}`} - - - - - - - ) -} - -const PrintableResponseContainer = forwardRef((_, ref) => { - const { data: form } = useAdminForm() - const { data, isLoading, isError } = useIndividualSubmission() - if (isLoading || isError || !form || !data?.responses) return null - - return ( - - - - ) -}) - -export default PrintableResponseContainer diff --git a/frontend/src/features/admin-form/responses/IndividualResponsePage/utils/generatePdf.ts b/frontend/src/features/admin-form/responses/IndividualResponsePage/utils/generatePdf.ts new file mode 100644 index 0000000000..c5ea614791 --- /dev/null +++ b/frontend/src/features/admin-form/responses/IndividualResponsePage/utils/generatePdf.ts @@ -0,0 +1,258 @@ +import pdfMake from 'pdfmake/build/pdfmake' +import pdfFonts from 'pdfmake/build/vfs_fonts' +import { Content, TDocumentDefinitions } from 'pdfmake/interfaces' + +import { BasicField } from '~shared/types' +import { handleAddressResponseDisplay } from '~shared/utils/address' + +import { convertToSignatureSvgString } from '~utils/convertSignatureOutput' + +import { AugmentedDecryptedResponse } from '../../ResponsesPage/storage/utils/augmentDecryptedResponses' + +const isTest = import.meta.env.STORYBOOK_NODE_ENV === 'test' +const MOCK_CONSTANT_FORM_LINK = 'https://form.gov.sg/64e2f7ae841cbe0012e785e7' + +const GREY_HEADER_BG_COLOR = '#484848' +const HEADER_TEXT_COLOR = 'white' +const FIELD_SEPARATOR_COLOR = '#eee' +const FIELD_CELL_PADDING = 9 +const HEADER_CELL_VERTICAL_PADDING = 30 + +pdfMake.vfs = pdfFonts.vfs +pdfMake.tableLayouts = { + header: { + hLineWidth: () => 3, + hLineColor: () => GREY_HEADER_BG_COLOR, + vLineWidth: () => 0, + paddingTop: (i) => (i === 0 ? HEADER_CELL_VERTICAL_PADDING : 0), + paddingBottom: (i) => (i === 1 ? HEADER_CELL_VERTICAL_PADDING : 0), + }, + field: { + hLineWidth: (i) => (i > 0 ? 0 : 1), + hLineColor: () => FIELD_SEPARATOR_COLOR, + vLineWidth: () => 0, + paddingTop: () => FIELD_CELL_PADDING, + paddingBottom: () => FIELD_CELL_PADDING, + paddingLeft: () => FIELD_CELL_PADDING, + paddingRight: () => FIELD_CELL_PADDING, + }, +} + +interface FieldRowRenderData { + question: string + answer: string +} + +/** + * Default rows to be included in response pdf. + */ +const getDefaultFieldRows = ({ + responseId, + submissionTime, +}: { + responseId: string + submissionTime: string +}): FieldRowRenderData[] => [ + { + question: 'Response ID', + answer: responseId, + }, + { + question: 'Time Submitted', + answer: submissionTime, + }, +] + +const getStandardFieldDocDefinition = (row: FieldRowRenderData) => { + return { + table: { + widths: ['*', '*'], + body: [ + [ + { text: row.question, style: 'field' }, + { text: row.answer, style: 'field' }, + ], + ], + }, + layout: 'field', + margin: [22.5, 0, 22.5, 0] as [number, number, number, number], + } +} + +const getSignatureFieldDocDefinition = (row: FieldRowRenderData) => { + const SIGNATURE_PDF_FIXED_WIDTH_PT = 225 // Same as the backend template's signature width (px converted to pt) + return { + table: { + widths: ['*', '*'], + body: [ + [ + { text: row.question, style: 'field' }, + { svg: row.answer, width: SIGNATURE_PDF_FIXED_WIDTH_PT }, + ], + ], + }, + layout: 'field', + margin: [22.5, 0, 22.5, 0] as [number, number, number, number], + } +} + +const getFieldDocDefinitionsFromResponses = ({ + fieldResponses, +}: { + fieldResponses: AugmentedDecryptedResponse[] +}): Content[] => { + const fieldDocDefinitions: Content[] = [] + for (const fieldResponse of fieldResponses) { + switch (fieldResponse.fieldType) { + case BasicField.Section: + fieldDocDefinitions.push({ + text: fieldResponse.question, + }) + break + case BasicField.Address: { + const transformedAddress = handleAddressResponseDisplay( + fieldResponse.answerArray as string[], + ).join(', ') + fieldDocDefinitions.push( + getStandardFieldDocDefinition({ + question: fieldResponse.question, + answer: transformedAddress, + }), + ) + break + } + case BasicField.Table: + if (fieldResponse.answerArray) { + const tableFieldDocDefinitions = fieldResponse.answerArray.map( + (ans) => + getStandardFieldDocDefinition({ + question: fieldResponse.question, + answer: Array.isArray(ans) ? ans.join(', ') : ans, + }), + ) + fieldDocDefinitions.push(...tableFieldDocDefinitions) + } + break + case BasicField.Signature: { + if ( + fieldResponse.answerArray && + fieldResponse.answerArray.length > 1 && + fieldResponse.answerArray[0] === 'draw' && + fieldResponse.answerArray[1].length > 0 + ) { + const signatureVectorArray = JSON.parse( + fieldResponse.answerArray[1] as string, + ) + fieldDocDefinitions.push( + getSignatureFieldDocDefinition({ + question: fieldResponse.question, + answer: convertToSignatureSvgString(signatureVectorArray), + }), + ) + } + break + } + default: + fieldDocDefinitions.push( + getStandardFieldDocDefinition({ + question: fieldResponse.question, + answer: + fieldResponse.answer || + fieldResponse.answerArray?.join(', ') || + '', + }), + ) + } + } + return fieldDocDefinitions +} + +const getDocDefinition = ({ + formTitle, + formId, + submissionId, + responses, + submissionTime, +}: ResponsePdfRenderData): TDocumentDefinitions => { + const FORM_URL = isTest + ? MOCK_CONSTANT_FORM_LINK + : `${window.location.origin}/${formId}` + + return { + styles: { + header: { + alignment: 'center', + color: HEADER_TEXT_COLOR, + fillColor: GREY_HEADER_BG_COLOR, + }, + field: { + alignment: 'left', + fontSize: 10.5, + }, + }, + content: [ + { + table: { + widths: ['*'], + body: [ + [ + { + text: formTitle, + style: 'header', + fontSize: 22.5, + marginBottom: 4, + }, + ], + [ + { + text: FORM_URL, + style: 'header', + fontSize: 8, + decoration: 'underline', + bold: true, + }, + ], + ], + }, + layout: 'header', + margin: [0, 0, 0, 22.5], + }, + ...getDefaultFieldRows({ responseId: submissionId, submissionTime }).map( + (row) => getStandardFieldDocDefinition(row), + ), + ...getFieldDocDefinitionsFromResponses({ fieldResponses: responses }), + ], + pageSize: 'A4', + pageMargins: [0, 16], + pageOrientation: 'portrait', + } +} + +interface ResponsePdfRenderData { + formId: string + formTitle: string + submissionId: string + responses: AugmentedDecryptedResponse[] + submissionTime: string +} + +const generatePdf = ({ + formId, + formTitle, + submissionId, + responses, + submissionTime, +}: ResponsePdfRenderData) => { + const pdfTitle = `${formTitle}_formId_${formId}_submissionId_${submissionId}_response.pdf` + + const docDefinition = getDocDefinition({ + formId, + formTitle, + submissionId, + responses, + submissionTime, + }) + return pdfMake.createPdf(docDefinition).download(pdfTitle) +} + +export default generatePdf