feat: add PDF generation and email sending for printable announcements

- Updated package.json to include html2canvas-pro and jsPDF for PDF generation.
- Enhanced SchedaAnnuncioStampabile component to generate a PDF from HTML and send it via email.
- Implemented new API endpoint for sending the generated PDF as an email attachment.
- Refactored existing email sending logic across various controllers to use a unified template structure.
- Improved error handling and logging for email sending processes.
This commit is contained in:
Marco Pedone 2025-11-19 17:18:28 +01:00
parent b75e7f2412
commit e5c0ca04ee
12 changed files with 713 additions and 251 deletions

View file

@ -49,9 +49,11 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.8.0", "framer-motion": "^12.8.0",
"frimousse": "^0.3.0", "frimousse": "^0.3.0",
"html2canvas-pro": "^1.5.13",
"ioredis": "^5.7.0", "ioredis": "^5.7.0",
"jose": "^6.0.12", "jose": "^6.0.12",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jspdf": "^3.0.4",
"kysely": "^0.28.2", "kysely": "^0.28.2",
"kysely-plugin-serialize": "^0.8.2", "kysely-plugin-serialize": "^0.8.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@ -76,7 +78,7 @@
"react-is": "^19.1.1", "react-is": "^19.1.1",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-number-format": "^5.4.4", "react-number-format": "^5.4.4",
"react-pdf": "^10.1.0", "react-pdf": "^10.2.0",
"react-phone-number-input": "^3.4.12", "react-phone-number-input": "^3.4.12",
"react-reverse-portal": "^2.3.0", "react-reverse-portal": "^2.3.0",
"react-select": "^5.10.2", "react-select": "^5.10.2",
@ -997,9 +999,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.28.3", "version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -2576,9 +2578,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@napi-rs/canvas": { "node_modules/@napi-rs/canvas": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.82.tgz",
"integrity": "sha512-0SkvRRjyxY35eniEsQsjPYUMWunKlAWvionJOzJJADZF5ZDf/sL+ncJbMTV5LUiHg1iHOvVjWcuDOx/GNXr/lA==", "integrity": "sha512-FGjyUBoF0sl1EenSiE4UV2WYu76q6F9GSYedq5EiOCOyGYoQ/Owulcv6rd7v/tWOpljDDtefXXIaOCJrVKem4w==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"workspaces": [ "workspaces": [
@ -2588,22 +2590,22 @@
"node": ">= 10" "node": ">= 10"
}, },
"optionalDependencies": { "optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.79", "@napi-rs/canvas-android-arm64": "0.1.82",
"@napi-rs/canvas-darwin-arm64": "0.1.79", "@napi-rs/canvas-darwin-arm64": "0.1.82",
"@napi-rs/canvas-darwin-x64": "0.1.79", "@napi-rs/canvas-darwin-x64": "0.1.82",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.79", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.82",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.79", "@napi-rs/canvas-linux-arm64-gnu": "0.1.82",
"@napi-rs/canvas-linux-arm64-musl": "0.1.79", "@napi-rs/canvas-linux-arm64-musl": "0.1.82",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.79", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.82",
"@napi-rs/canvas-linux-x64-gnu": "0.1.79", "@napi-rs/canvas-linux-x64-gnu": "0.1.82",
"@napi-rs/canvas-linux-x64-musl": "0.1.79", "@napi-rs/canvas-linux-x64-musl": "0.1.82",
"@napi-rs/canvas-win32-x64-msvc": "0.1.79" "@napi-rs/canvas-win32-x64-msvc": "0.1.82"
} }
}, },
"node_modules/@napi-rs/canvas-android-arm64": { "node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.82.tgz",
"integrity": "sha512-ih6ZIztNDEXl7axvC4swOwLFrM9lOyJa9VAMq7xIBtEZhR/8IVDa0ZTup2fZEiTCmnjmXolzv7uDviHkOTEMKQ==", "integrity": "sha512-bvZhN0iI54ouaQOrgJV96H2q7J3ZoufnHf4E1fUaERwW29Rz4rgicohnAg4venwBJZYjGl5Yl3CGmlAl1LZowQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2617,9 +2619,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-darwin-arm64": { "node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.82.tgz",
"integrity": "sha512-REMz1Fac2VlOYJDg+JjmQWSJc459cCgVom6GvKwWkDqzSjvG9BSo72MDmQY3uhb7r49Xuz5gTFcLYTfNcm4MoA==", "integrity": "sha512-InuBHKCyuFqhNwNr4gpqazo5Xp6ltKflqOLiROn4hqAS8u21xAHyYCJRgHwd+a5NKmutFTaRWeUIT/vxWbU/iw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2633,9 +2635,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-darwin-x64": { "node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.82.tgz",
"integrity": "sha512-uQxLg6Bll7zv/ljp/YIeiUFWfV9C/ESv+2ioUh60hIAypuhtg6hhtWE/KnoW7G48wQls5VUStvEnJbnJ7bPKlA==", "integrity": "sha512-aQGV5Ynn96onSXcuvYb2y7TRXD/t4CL2EGmnGqvLyeJX1JLSNisKQlWN/1bPDDXymZYSdUqbXehj5qzBlOx+RQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2649,9 +2651,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.82.tgz",
"integrity": "sha512-X37B//TVIipL/3RyvyfNlbQK2uyIaK3PJ2bH7ZeU+jpkaYprBsV15GCN/LHTYAi6R0F/c53zK3aSFNKkGHM/Og==", "integrity": "sha512-YIUpmHWeHGGRhWitT1KJkgj/JPXPfc9ox8oUoyaGPxolLGPp5AxJkq8wIg8CdFGtutget968dtwmx71m8o3h5g==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -2665,9 +2667,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-arm64-gnu": { "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.82.tgz",
"integrity": "sha512-+T1fuau1heabE6zGXiqZBGPH5fTIQF+xEu/u4fuugxEiChRYlhnPjkw26MBi8ePg/jmzxLfJEij6LMJQ4AQa2A==", "integrity": "sha512-AwLzwLBgmvk7kWeUgItOUor/QyG31xqtD26w1tLpf4yE0hiXTGp23yc669aawjB6FzgIkjh1NKaNS52B7/qEBQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2681,9 +2683,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-arm64-musl": { "node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.82.tgz",
"integrity": "sha512-KsrsR3+6uXv70W/1/kY0yRK4/bbdJgA1Vuxw4KyfSc6mjl1DMoYXDAjpBT/5w7AXy6cGG44jm3upvvt/y/dPfg==", "integrity": "sha512-moZWuqepAwWBffdF4JDadt8TgBD02iMhG6I1FHZf8xO20AsIp9rB+p0B8Zma2h2vAF/YMjeFCDmW5un6+zZz9g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2697,9 +2699,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": { "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.82.tgz",
"integrity": "sha512-EXaENnSJD6au6z4aKN2PpU9eVNWUsRI2cApm8gCa0WSRMaiYXZsFkXQmhB+Vz2pXahOS8BN2Zd8S1IeML/LCtg==", "integrity": "sha512-w9++2df2kG9eC9LWYIHIlMLuhIrKGQYfUxs97CwgxYjITeFakIRazI9LYWgVzEc98QZ9x9GQvlicFsrROV59MQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -2713,9 +2715,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-x64-gnu": { "node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.82.tgz",
"integrity": "sha512-3xZhHlE9e3cd9D7Comy6/TTSs/8PUGXEXymIwYQrA1QxHojAlAOFlVai4rffzXd0bHylZu+/wD76LodvYqF1Yw==", "integrity": "sha512-lZulOPwrRi6hEg/17CaqdwWEUfOlIJuhXxincx1aVzsVOCmyHf+xFq4i6liJl1P+x2v6Iz2Z/H5zHvXJCC7Bwg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2729,9 +2731,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-x64-musl": { "node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.82.tgz",
"integrity": "sha512-4yv550uCjIEoTFgrpxYZK67nFlDMCQa3LAheM2QrO+B8w1p5w04usIQSCHqHe6aPWlbLQCIqfVcew6/7Q4KuHg==", "integrity": "sha512-Be9Wf5RTv1w6GXlTph55K3PH3vsAh1Ax4T1FQY1UYM0QfD0yrwGdnJ8/fhqw7dEgMjd59zIbjJQC8C3msbGn5g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2745,9 +2747,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-win32-x64-msvc": { "node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.79", "version": "0.1.82",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.79.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.82.tgz",
"integrity": "sha512-sD5qP2njBRnhNlTNFJDdpeCN6aR3qVamLySTwhX3ec8sdfeT/chf/x2dw2UXoIGMoVaVk/y2ifwxBj/h2a2jug==", "integrity": "sha512-LN/i8VrvxTDmEEK1c10z2cdOTkWT76LlTGtyZe5Kr1sqoSomKeExAjbilnu1+oee5lZUgS5yfZ2LNlVhCeARuw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -9207,6 +9209,12 @@
"integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@ -9231,6 +9239,13 @@
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.1.10", "version": "19.1.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
@ -9271,6 +9286,13 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"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==",
"license": "MIT",
"optional": true
},
"node_modules/@types/use-sync-external-store": { "node_modules/@types/use-sync-external-store": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@ -9795,6 +9817,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -10041,6 +10072,26 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -10351,6 +10402,18 @@
"toggle-selection": "^1.0.6" "toggle-selection": "^1.0.6"
} }
}, },
"node_modules/core-js": {
"version": "3.47.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
"integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.5", "version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@ -10442,6 +10505,15 @@
"hyphenate-style-name": "^1.0.3" "hyphenate-style-name": "^1.0.3"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-select": { "node_modules/css-select": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@ -10917,6 +10989,16 @@
"url": "https://github.com/fb55/domhandler?sponsor=1" "url": "https://github.com/fb55/domhandler?sponsor=1"
} }
}, },
"node_modules/dompurify": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": { "node_modules/domutils": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@ -11469,6 +11551,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-shallow-equal": { "node_modules/fast-shallow-equal": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
@ -11536,6 +11629,12 @@
"walk-up-path": "^4.0.0" "walk-up-path": "^4.0.0"
} }
}, },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -12118,6 +12217,33 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/html2canvas-pro": {
"version": "1.5.13",
"resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-1.5.13.tgz",
"integrity": "sha512-//ksb3JLA+ewSxFODyb5YH6xqRLpe6Kt1i7Zyr5eqHmfcNt0bm8MpzSQjOK2qu6cCv13wqWtOZrCOpA+//EhaA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@ -12251,6 +12377,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/ioredis": { "node_modules/ioredis": {
"version": "5.7.0", "version": "5.7.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz",
@ -12879,6 +13011,23 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jspdf": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/kanel": { "node_modules/kanel": {
"version": "3.14.4", "version": "3.14.4",
"resolved": "https://registry.npmjs.org/kanel/-/kanel-3.14.4.tgz", "resolved": "https://registry.npmjs.org/kanel/-/kanel-3.14.4.tgz",
@ -14713,6 +14862,12 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -14812,15 +14967,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/pdfjs-dist": { "node_modules/pdfjs-dist": {
"version": "5.3.93", "version": "5.4.296",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.3.93.tgz", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
"integrity": "sha512-w3fQKVL1oGn8FRyx5JUG5tnbblggDqyx2XzA5brsJ5hSuS+I0NdnJANhmeWKLjotdbPQucLBug5t0MeWr0AAdg==", "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=20.16.0 || >=22.3.0" "node": ">=20.16.0 || >=22.3.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@napi-rs/canvas": "^0.1.71" "@napi-rs/canvas": "^0.1.80"
} }
}, },
"node_modules/peberminta": { "node_modules/peberminta": {
@ -14832,6 +14987,13 @@
"url": "https://ko-fi.com/killymxi" "url": "https://ko-fi.com/killymxi"
} }
}, },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/pg": { "node_modules/pg": {
"version": "8.16.3", "version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
@ -15701,6 +15863,16 @@
} }
} }
}, },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/ramda": { "node_modules/ramda": {
"version": "0.30.1", "version": "0.30.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz",
@ -16124,9 +16296,9 @@
} }
}, },
"node_modules/react-pdf": { "node_modules/react-pdf": {
"version": "10.1.0", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.1.0.tgz", "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.2.0.tgz",
"integrity": "sha512-iUI1YqWgwwZcsXjrehTp3Yi8nT/bvTaWULaRMMyJWvoqqSlopk4LQQ9GDqUnDtX3gzT2glrqrLbjIPl56a+Q3w==", "integrity": "sha512-zk0DIL31oCh8cuQycM0SJKfwh4Onz0/Nwi6wTOjgtEjWGUY6eM+/vuzvOP3j70qtEULn7m1JtaeGzud1w5fY2Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"clsx": "^2.0.0", "clsx": "^2.0.0",
@ -16134,7 +16306,7 @@
"make-cancellable-promise": "^2.0.0", "make-cancellable-promise": "^2.0.0",
"make-event-props": "^2.0.0", "make-event-props": "^2.0.0",
"merge-refs": "^2.0.0", "merge-refs": "^2.0.0",
"pdfjs-dist": "5.3.93", "pdfjs-dist": "5.4.296",
"tiny-invariant": "^1.0.0", "tiny-invariant": "^1.0.0",
"warning": "^4.0.0" "warning": "^4.0.0"
}, },
@ -16552,6 +16724,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@ -16650,6 +16829,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
@ -17407,6 +17596,16 @@
"stackframe": "^1.3.4" "stackframe": "^1.3.4"
} }
}, },
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/stackframe": { "node_modules/stackframe": {
"version": "1.3.4", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
@ -17880,6 +18079,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/tagged-comment-parser": { "node_modules/tagged-comment-parser": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/tagged-comment-parser/-/tagged-comment-parser-1.3.8.tgz", "resolved": "https://registry.npmjs.org/tagged-comment-parser/-/tagged-comment-parser-1.3.8.tgz",
@ -18025,6 +18234,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/thenify": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -18427,6 +18645,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",

View file

@ -61,9 +61,11 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.8.0", "framer-motion": "^12.8.0",
"frimousse": "^0.3.0", "frimousse": "^0.3.0",
"html2canvas-pro": "^1.5.13",
"ioredis": "^5.7.0", "ioredis": "^5.7.0",
"jose": "^6.0.12", "jose": "^6.0.12",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jspdf": "^3.0.4",
"kysely": "^0.28.2", "kysely": "^0.28.2",
"kysely-plugin-serialize": "^0.8.2", "kysely-plugin-serialize": "^0.8.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@ -88,7 +90,7 @@
"react-is": "^19.1.1", "react-is": "^19.1.1",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-number-format": "^5.4.4", "react-number-format": "^5.4.4",
"react-pdf": "^10.1.0", "react-pdf": "^10.2.0",
"react-phone-number-input": "^3.4.12", "react-phone-number-input": "^3.4.12",
"react-reverse-portal": "^2.3.0", "react-reverse-portal": "^2.3.0",
"react-select": "^5.10.2", "react-select": "^5.10.2",

View file

@ -1,5 +1,7 @@
"use client"; "use client";
import html2canvas from "html2canvas-pro";
import jsPDF from "jspdf";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useReactToPrint } from "react-to-print"; import { useReactToPrint } from "react-to-print";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
@ -10,6 +12,7 @@ import { handleConsegna } from "~/lib/annuncio_details";
import { replaceWithBr } from "~/lib/newlineToBr"; import { replaceWithBr } from "~/lib/newlineToBr";
import { formatCurrency } from "~/lib/utils"; import { formatCurrency } from "~/lib/utils";
import type { Annunci } from "~/schemas/public/Annunci"; import type { Annunci } from "~/schemas/public/Annunci";
import { api } from "~/utils/api";
import { IconMatrix } from "./IconComponents"; import { IconMatrix } from "./IconComponents";
import { GoogleMapsIcon, LogoSvg, WhatsAppIcon } from "./svgs"; import { GoogleMapsIcon, LogoSvg, WhatsAppIcon } from "./svgs";
@ -20,6 +23,119 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
//print: async (target) => {console.log("Printing...", target.contentDocument);}, //print: async (target) => {console.log("Printing...", target.contentDocument);},
}); });
const { mutate } = api.comunicazioni.sendSchedaAnnuncioEmail.useMutation();
const reactToPrintFn2 = useReactToPrint({
contentRef,
print: async (target) => {
try {
const htmlContent = target.contentDocument?.documentElement;
if (!htmlContent) return;
htmlContent.querySelectorAll(".print\\:border-none").forEach((el) => {
el.classList.remove("border");
});
htmlContent.querySelectorAll(".print\\:shadow-none").forEach((el) => {
el.classList.remove("shadow-sm");
});
const canvas = await html2canvas(htmlContent, {
//scale: 2,
useCORS: true,
logging: false,
backgroundColor: "#ffffff",
});
const imgData = canvas.toDataURL("image/png", 0.85);
const pdf = new jsPDF("p", "mm", "a4");
// A4 dimensions in mm
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
// Define margins (in mm)
const marginTop = 5;
const marginRight = 5;
const marginBottom = 5;
const marginLeft = 5;
// Calculate available space
const availableWidth = pdfWidth - marginLeft - marginRight;
const availableHeight = pdfHeight - marginTop - marginBottom;
// Calculate image dimensions
const imgProps = pdf.getImageProperties(imgData);
const imgWidth = imgProps.width;
const imgHeight = imgProps.height;
// Calculate how the image should fit with margins
const ratio = availableWidth / imgWidth;
const scaledHeight = imgHeight * ratio;
// Check if content needs multiple pages
if (scaledHeight <= availableHeight) {
// Single page - content fits
pdf.addImage(
imgData,
"PNG",
marginLeft,
marginTop,
availableWidth,
scaledHeight,
undefined,
"FAST",
);
} else {
// Multiple pages needed
let heightLeft = scaledHeight;
let position = marginTop;
// Add first page
pdf.addImage(
imgData,
"PNG",
marginLeft,
position,
availableWidth,
scaledHeight,
undefined,
"FAST",
);
heightLeft -= availableHeight;
// Add remaining pages
while (heightLeft > 0) {
position = marginTop - (scaledHeight - heightLeft);
pdf.addPage();
pdf.addImage(
imgData,
"PNG",
marginLeft,
position,
availableWidth,
scaledHeight,
undefined,
"FAST",
);
heightLeft -= availableHeight;
}
}
const pdfBase64 = pdf.output("dataurlstring");
console.log(
"PDF size:",
((pdfBase64.length * 0.75) / 1024 / 1024).toFixed(2),
"MB",
);
mutate({
pdf: pdfBase64,
to: "m.pedone98@gmail.com",
codice: data.codice,
});
} catch (e) {
console.error("Error during print:", e);
}
},
});
return ( return (
<div className="mx-auto max-w-4xl"> <div className="mx-auto max-w-4xl">
<div className="space-y-6"> <div className="space-y-6">
@ -29,6 +145,9 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
<Button className="print:hidden" onClick={() => reactToPrintFn()}> <Button className="print:hidden" onClick={() => reactToPrintFn()}>
Stampa Stampa
</Button> </Button>
<Button className="print:hidden" onClick={() => reactToPrintFn2()}>
Stampa
</Button>
</div> </div>
<div ref={contentRef}> <div ref={contentRef}>
<SchedaAnnuncio data={data} /> <SchedaAnnuncio data={data} />
@ -38,7 +157,7 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
); );
} }
function SchedaAnnuncio({ data }: { data: Annunci }) { export function SchedaAnnuncio({ data }: { data: Annunci }) {
const companyAddress = const companyAddress =
"Via Beata Giovanna, 1 a Bassano del Grappa (VI)\nTel. +39 0424529869\nEmail: arca@infoalloggi.it"; "Via Beata Giovanna, 1 a Bassano del Grappa (VI)\nTel. +39 0424529869\nEmail: arca@infoalloggi.it";

View file

@ -1,6 +1,11 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "~/server/api/trpc";
import { GetEmails } from "~/server/services/email.service"; import { GetEmails } from "~/server/services/email.service";
import { NewMail } from "~/server/services/mailer";
import { zUserId } from "~/server/utils/zod_types"; import { zUserId } from "~/server/utils/zod_types";
export const comunicazioniRouter = createTRPCRouter({ export const comunicazioniRouter = createTRPCRouter({
@ -35,4 +40,45 @@ export const comunicazioniRouter = createTRPCRouter({
// userId: input.userId, // userId: input.userId,
// }); // });
// }), // }),
sendSchedaAnnuncioEmail: adminProcedure
.input(
z.object({
pdf: z.string(),
codice: z.string(),
to: z.email(),
}),
)
.mutation(async ({ input }) => {
try {
const base64Content = input.pdf.includes("base64,")
? input.pdf.split("base64,")[1]
: input.pdf;
await NewMail({
template: {
mailType: "generic",
props: {
noreply: true,
testo: `In allegato la scheda annuncio stampabile per l'annuncio con codice ${input.codice}.`,
title: "Scheda Annuncio Stampabile",
},
},
mail: {
subject: `Scheda Annuncio ${input.codice} - Infoalloggi.it`,
to: input.to,
attachments: [
{
filename: `scheda-annuncio-${input.codice}.pdf`,
content: base64Content,
encoding: "base64",
contentType: "application/pdf",
},
],
},
});
} catch (e) {
throw new Error(
`Errore nell'invio dell'email: ${(e as Error).message}`,
);
}
}),
}); });

View file

@ -22,6 +22,7 @@ export const contactRouter = createTRPCRouter({
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await NewMail({ await NewMail({
template: {
mailType: "contattoAdminEmail", mailType: "contattoAdminEmail",
props: { props: {
cognome: input.cognome, cognome: input.cognome,
@ -30,8 +31,11 @@ export const contactRouter = createTRPCRouter({
nome: input.nome, nome: input.nome,
telefono: input.telefono, telefono: input.telefono,
}, },
},
mail: {
subject: `Richiesta di contatto da ${input.nome}`, subject: `Richiesta di contatto da ${input.nome}`,
to: "web@infoalloggi.it", to: "web@infoalloggi.it",
},
}); });
return { success: true }; return { success: true };
}), }),

View file

@ -50,6 +50,7 @@ export const intrestsRouter = createTRPCRouter({
}); });
} }
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
link: { link: {
@ -59,8 +60,11 @@ export const intrestsRouter = createTRPCRouter({
testo: `L'utente ${user.username} (${user.email}) ha mostrato interesse per l'annuncio "${annuncio.titolo_it}" (Codice: ${annuncio.codice}).`, testo: `L'utente ${user.username} (${user.email}) ha mostrato interesse per l'annuncio "${annuncio.titolo_it}" (Codice: ${annuncio.codice}).`,
title: "Nuovo interesse per un annuncio", title: "Nuovo interesse per un annuncio",
}, },
},
mail: {
subject: "Utente interessato ad un annuncio", subject: "Utente interessato ad un annuncio",
to: "web@infoalloggi.it", to: "web@infoalloggi.it",
},
userId, userId,
}); });
return { success: true }; return { success: true };

View file

@ -76,11 +76,12 @@ export const processEvents = async () => {
const data = e.data as EventDataTypes; const data = e.data as EventDataTypes;
switch (data.tipo) { switch (data.tipo) {
case "email": case "email": {
await NewMail({ await NewMail({
...data.data, ...data.data,
}); });
break; break;
}
case "sms": case "sms":
await SendMessage(data.data); await SendMessage(data.data);

View file

@ -929,6 +929,7 @@ export const SbloccaContatti = async ({
const EmailProprietario: NewMailProps | null = data.annuncio.email const EmailProprietario: NewMailProps | null = data.annuncio.email
? { ? {
template: {
mailType: "emailContattoInteressato", mailType: "emailContattoInteressato",
props: { props: {
indirizzo: data.annuncio.indirizzo, indirizzo: data.annuncio.indirizzo,
@ -937,12 +938,16 @@ export const SbloccaContatti = async ({
sesso: data.anagrafica.sesso, sesso: data.anagrafica.sesso,
telefono: data.user.telefono, telefono: data.user.telefono,
}, },
},
mail: {
subject: "Un interessato ha ricevuto i Suoi contatti", subject: "Un interessato ha ricevuto i Suoi contatti",
to: data.annuncio.email, to: data.annuncio.email,
},
} }
: null; : null;
const EmailInteressato: NewMailProps = { const EmailInteressato: NewMailProps = {
template: {
mailType: "generic", mailType: "generic",
props: { props: {
link: { link: {
@ -953,8 +958,11 @@ export const SbloccaContatti = async ({
testo: `I contatti per l'annuncio ${data.annuncio.codice} sono stati sbloccati con successo.`, testo: `I contatti per l'annuncio ${data.annuncio.codice} sono stati sbloccati con successo.`,
title: "Contatti sbloccati per il servizio di ricerca affitto", title: "Contatti sbloccati per il servizio di ricerca affitto",
}, },
},
mail: {
subject: "Contatti sbloccati per il servizio di ricerca affitto", subject: "Contatti sbloccati per il servizio di ricerca affitto",
to: data.user.email, to: data.user.email,
},
}; };
const numeroProprietario = data.annuncio.numero const numeroProprietario = data.annuncio.numero
? data.annuncio.numero.replace(/\s+/g, "").split(",")[0] || null ? data.annuncio.numero.replace(/\s+/g, "").split(",")[0] || null
@ -1053,18 +1061,23 @@ export const interruzioneServizio = async (servizioId: ServizioServizioId) => {
// NOTIFICHE EMAIL INTERRUZIONE SERVIZIO // NOTIFICHE EMAIL INTERRUZIONE SERVIZIO
const utente = await getUser({ db, userId: servizio.user_id }); const utente = await getUser({ db, userId: servizio.user_id });
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
noreply: true, noreply: true,
testo: `Abbiamo ricevuto la tua richiesta di interruzione del servizio di ricerca affitto per la tipologia ${servizio.tipologia}. Il servizio è stato interrotto con successo.`, testo: `Abbiamo ricevuto la tua richiesta di interruzione del servizio di ricerca affitto per la tipologia ${servizio.tipologia}. Il servizio è stato interrotto con successo.`,
title: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto`, title: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto`,
}, },
},
mail: {
subject: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto`, subject: `Servizio Ricerca Affitto ${servizio.tipologia} interrotto`,
to: utente.email, to: utente.email,
},
userId: utente.id, userId: utente.id,
}); });
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
link: { link: {
@ -1075,8 +1088,11 @@ export const interruzioneServizio = async (servizioId: ServizioServizioId) => {
testo: `L'utente ha richiesto l'interruzione del servizio di ricerca affitto per la tipologia ${servizio.tipologia}. Puoi procedere con le prossime azioni.`, testo: `L'utente ha richiesto l'interruzione del servizio di ricerca affitto per la tipologia ${servizio.tipologia}. Puoi procedere con le prossime azioni.`,
title: `Utente ${utente.username} ha richieto l'interruzione del servizio di ricerca affitto`, title: `Utente ${utente.username} ha richieto l'interruzione del servizio di ricerca affitto`,
}, },
},
mail: {
subject: `Utente ${utente.username} ha richieto l'interruzione del servizio di ricerca affitto`, subject: `Utente ${utente.username} ha richieto l'interruzione del servizio di ricerca affitto`,
to: "web@infoalloggi.it", to: "web@infoalloggi.it",
},
}); });
return true; return true;
@ -1174,6 +1190,7 @@ export const sendServizioEmail = async ({
const { email, nome } = user; const { email, nome } = user;
await NewMail({ await NewMail({
template: {
mailType: "onboardingServizio", mailType: "onboardingServizio",
props: { props: {
annunci: annunci.map((annuncio) => ({ annunci: annunci.map((annuncio) => ({
@ -1187,8 +1204,11 @@ export const sendServizioEmail = async ({
nome, nome,
token: servizioId, token: servizioId,
}, },
},
mail: {
subject: "Procedi ora su Infoalloggi.it", subject: "Procedi ora su Infoalloggi.it",
to: email, to: email,
},
userId, userId,
}); });
await db await db
@ -1247,6 +1267,7 @@ export const userSendConfermaIntent = async ({
.executeTakeFirstOrThrow(); .executeTakeFirstOrThrow();
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
link: { link: {
@ -1257,19 +1278,26 @@ export const userSendConfermaIntent = async ({
testo: `L'utente ${utente.username} ha confermato l'immobile con codice ${annuncio_codice.codice}.`, testo: `L'utente ${utente.username} ha confermato l'immobile con codice ${annuncio_codice.codice}.`,
title: `Nuova conferma immobile ${annuncio_codice.codice}`, title: `Nuova conferma immobile ${annuncio_codice.codice}`,
}, },
},
mail: {
subject: `Nuova conferma immobile ${annuncio_codice.codice}`, subject: `Nuova conferma immobile ${annuncio_codice.codice}`,
to: "web@infoalloggi.it", to: "web@infoalloggi.it",
},
}); });
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
noreply: true, noreply: true,
testo: `La tua conferma per l'immobile con codice ${annuncio_codice.codice} è stata ricevuta. Il nostro team la esaminerà a breve. Riceverai una notifica via email per procedere.`, testo: `La tua conferma per l'immobile con codice ${annuncio_codice.codice} è stata ricevuta. Il nostro team la esaminerà a breve. Riceverai una notifica via email per procedere.`,
title: `Conferma immobile ${annuncio_codice.codice} ricevuta`, title: `Conferma immobile ${annuncio_codice.codice} ricevuta`,
}, },
},
mail: {
subject: `Conferma immobile ${annuncio_codice.codice} ricevuta`, subject: `Conferma immobile ${annuncio_codice.codice} ricevuta`,
to: utente.email, to: utente.email,
},
userId: utente.id, userId: utente.id,
}); });
return true; return true;
@ -1305,6 +1333,7 @@ export const adminUpdateConfermaStatus = async ({
//ADMIN CONFERMA LAVORATA EMAIL //ADMIN CONFERMA LAVORATA EMAIL
if (status) { if (status) {
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
link: { link: {
@ -1315,8 +1344,11 @@ export const adminUpdateConfermaStatus = async ({
testo: `La conferma è stata lavorata con successo. Puoi procedere con le prossime azioni.`, testo: `La conferma è stata lavorata con successo. Puoi procedere con le prossime azioni.`,
title: "Conferma lavorata per il servizio di ricerca affitto", title: "Conferma lavorata per il servizio di ricerca affitto",
}, },
},
mail: {
subject: "Conferma lavorata per il servizio di ricerca affitto", subject: "Conferma lavorata per il servizio di ricerca affitto",
to: utente.email, to: utente.email,
},
userId: utente.id, userId: utente.id,
}); });
} }
@ -1353,6 +1385,7 @@ export const userAccettaConferma = async ({
// USER ACCETTA CONFERMA EMAIL // USER ACCETTA CONFERMA EMAIL
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
link: { link: {
@ -1363,8 +1396,11 @@ export const userAccettaConferma = async ({
testo: `L'utente ha accettato le condizioni di conferma per la locazione. Puoi procedere con le prossime azioni.`, testo: `L'utente ha accettato le condizioni di conferma per la locazione. Puoi procedere con le prossime azioni.`,
title: `Utente ${utente.username} ha accettato le condizioni di conferma`, title: `Utente ${utente.username} ha accettato le condizioni di conferma`,
}, },
},
mail: {
subject: `Utente ${utente.username} ha accettato le condizioni di conferma`, subject: `Utente ${utente.username} ha accettato le condizioni di conferma`,
to: "web@infoalloggi.it", to: "web@infoalloggi.it",
},
}); });
return true; return true;
@ -1552,6 +1588,7 @@ export const SendContactEmail = async ({
}) => { }) => {
try { try {
await NewMail({ await NewMail({
template: {
mailType: "contattoAnnuncioEmail", mailType: "contattoAnnuncioEmail",
props: { props: {
codice, codice,
@ -1560,8 +1597,11 @@ export const SendContactEmail = async ({
nome, nome,
telefono, telefono,
}, },
},
mail: {
subject: "Contatto per annuncio", subject: "Contatto per annuncio",
to: "web@infoalloggi.it", to: "web@infoalloggi.it",
},
}); });
return { return {
email, email,

View file

@ -155,13 +155,17 @@ export const SendMessage = async (data: MinimalSMSData) => {
console.error("Error:", error); console.error("Error:", error);
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
testo: `Errore durante l'invio dell'SMS: to:${data.recipient.join(", ")} Mgs:${data.message}; ${error instanceof Error ? error.message : String(error)}`, testo: `Errore durante l'invio dell'SMS: to:${data.recipient.join(", ")} Mgs:${data.message}; ${error instanceof Error ? error.message : String(error)}`,
title: "Errore invio SMS", title: "Errore invio SMS",
}, },
},
mail: {
subject: "Errore invio SMS", subject: "Errore invio SMS",
to: "web@infoalloggi.it", to: "web@infoalloggi.it",
},
}); });
} }
}; };

View file

@ -135,6 +135,7 @@ export const whIntentSucceededHandler = async ({
.execute(); .execute();
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
@ -146,8 +147,11 @@ export const whIntentSucceededHandler = async ({
testo: `Il pagamento dell'acconto per il servizio ${ordine.packid} è stato confermato.`, testo: `Il pagamento dell'acconto per il servizio ${ordine.packid} è stato confermato.`,
title: "Pagamento Confermato ", title: "Pagamento Confermato ",
}, },
},
mail: {
subject: "Pagamento Confermato - Acconto Infoalloggi.it", subject: "Pagamento Confermato - Acconto Infoalloggi.it",
to: utente.email, to: utente.email,
},
userId: utente.id, userId: utente.id,
}); });
break; break;
@ -163,6 +167,7 @@ export const whIntentSucceededHandler = async ({
}) })
.execute(); .execute();
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
link: { link: {
@ -173,8 +178,11 @@ export const whIntentSucceededHandler = async ({
testo: `Il pagamento del saldo per il servizio ${ordine.packid} è stato confermato.`, testo: `Il pagamento del saldo per il servizio ${ordine.packid} è stato confermato.`,
title: "Pagamento Confermato ", title: "Pagamento Confermato ",
}, },
},
mail: {
subject: "Pagamento Confermato - Saldo Infoalloggi.it", subject: "Pagamento Confermato - Saldo Infoalloggi.it",
to: utente.email, to: utente.email,
},
userId: utente.id, userId: utente.id,
}); });
break; break;
@ -187,6 +195,7 @@ export const whIntentSucceededHandler = async ({
}) })
.execute(); .execute();
await NewMail({ await NewMail({
template: {
mailType: "generic", mailType: "generic",
props: { props: {
link: { link: {
@ -197,20 +206,27 @@ export const whIntentSucceededHandler = async ({
testo: `Il pagamento per la consulenza è stato confermato.`, testo: `Il pagamento per la consulenza è stato confermato.`,
title: "Pagamento Confermato ", title: "Pagamento Confermato ",
}, },
},
mail: {
subject: "Pagamento Confermato - Consulenza Infoalloggi.it", subject: "Pagamento Confermato - Consulenza Infoalloggi.it",
to: utente.email, to: utente.email,
},
userId: utente.id, userId: utente.id,
}); });
break; break;
case OrderTypeEnum.Altro: case OrderTypeEnum.Altro:
// TODO: Handle other types // TODO: Handle other types
await NewMail({ await NewMail({
template: {
mailType: "pagamentoConferma", mailType: "pagamentoConferma",
props: { props: {
receiptHref: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`, receiptHref: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`,
}, },
},
mail: {
subject: "Pagamento Confermato", subject: "Pagamento Confermato",
to: utente.email, to: utente.email,
},
userId: utente.id, userId: utente.id,
}); });
break; break;
@ -246,9 +262,13 @@ export const whIntentFailedHandler = async ({
}); });
await NewMail({ await NewMail({
template: {
mailType: "pagamentoErrore", mailType: "pagamentoErrore",
},
mail: {
subject: "Pagamento Fallito", subject: "Pagamento Fallito",
to: utente.email, to: utente.email,
},
userId: utente.id, userId: utente.id,
}); });
} }

View file

@ -115,12 +115,16 @@ export const resetPasswordFromTokenHandler = async ({
export const pwResetSendLinkHandler = async ({ email }: { email: string }) => { export const pwResetSendLinkHandler = async ({ email }: { email: string }) => {
const { token, userId } = await pwResetTokenFromMailHandler({ email }); const { token, userId } = await pwResetTokenFromMailHandler({ email });
await NewMail({ await NewMail({
template: {
mailType: "pwResetLink", mailType: "pwResetLink",
props: { props: {
resetlink: `${env.BASE_URL}/auth/password-reset/${token}`, resetlink: `${env.BASE_URL}/auth/password-reset/${token}`,
}, },
},
mail: {
subject: `Richiesta di reset password Infoalloggi.it`, subject: `Richiesta di reset password Infoalloggi.it`,
to: email, to: email,
},
userId, userId,
}); });
}; };

View file

@ -38,12 +38,7 @@ import { GetFlagValueHandler } from "~/server/controllers/flags.controller";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { StoreEmail } from "~/server/services/email.service"; import { StoreEmail } from "~/server/services/email.service";
type MailerProps = { const mailer = async (options: Options) => {
to: string;
subject: string;
html: string;
};
const mailer = async ({ to, subject, html }: MailerProps) => {
const smtpTransport = createTransport({ const smtpTransport = createTransport({
auth: { auth: {
pass: env.ARUBA_PASS, pass: env.ARUBA_PASS,
@ -63,9 +58,7 @@ const mailer = async ({ to, subject, html }: MailerProps) => {
const message: Options = { const message: Options = {
from: `"InfoAlloggi" <${env.ARUBA_USER}>`, from: `"InfoAlloggi" <${env.ARUBA_USER}>`,
html: html, ...options,
subject: subject,
to: to,
}; };
smtpTransport.sendMail(message, (err, info) => { smtpTransport.sendMail(message, (err, info) => {
if (err) { if (err) {
@ -75,13 +68,11 @@ const mailer = async ({ to, subject, html }: MailerProps) => {
}); });
}; };
type base_NewMailProps = { export type NewMailProps = {
template?: MailsTemplates;
userId?: UsersId; userId?: UsersId;
to: string; mail: Options;
subject: string;
}; };
export type NewMailProps = base_NewMailProps & MailsTemplates;
export type MailsTemplates = export type MailsTemplates =
| ContattoAdmin_NewMail | ContattoAdmin_NewMail
| ContattoAnnuncio_NewMail | ContattoAnnuncio_NewMail
@ -185,24 +176,24 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
return { html: await render(mailComponent), title }; return { html: await render(mailComponent), title };
}; };
export const NewMail = async ({ export const NewMail = async ({ template, userId, mail }: NewMailProps) => {
userId,
to,
subject,
...mail
}: NewMailProps) => {
const emailFlag = await GetFlagValueHandler({ const emailFlag = await GetFlagValueHandler({
db: db, db: db,
id: "EMAIL_OFF", id: "EMAIL_OFF",
}); });
if (emailFlag === "false") { if (emailFlag === "false") {
try { try {
const { html } = await genMail({ ...mail }); if (template) {
const { html } = await genMail({ ...template });
await mailer({ await mailer({
html, html,
subject: subject, ...mail,
to: to,
}); });
} else {
await mailer({
...mail,
});
}
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
@ -211,12 +202,12 @@ export const NewMail = async ({
} }
} else { } else {
console.warn( console.warn(
`Email sending is disabled to:${to} subj:${subject} msgType: ${mail.mailType}`, `Email sending is disabled to:${mail.to} subj:${mail.subject} msgType: ${template?.mailType}`,
); );
} }
if (userId) { if (userId && template) {
// Store the email in the database if userId is provided // Store the email in the database if userId is provided
await StoreEmail({ data: mail, userId }); await StoreEmail({ data: template, userId });
} }
}; };