Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52008f5b43 | |||
| 5b57cdd6bf | |||
| 60eadf0847 | |||
| 682ccfd41f | |||
| 59c6b19ad6 | |||
| aa1a4cf6ff | |||
| 5f0fad3cbb | |||
| 545fae59b2 | |||
| d748d2db21 | |||
| 095c91e56d |
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
Generated
+529
@@ -17,7 +17,9 @@
|
|||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"morgan": "1.10.0",
|
"morgan": "1.10.0",
|
||||||
"nodemailer": "^8.0.5",
|
"nodemailer": "^8.0.5",
|
||||||
|
"pdfkit": "^0.18.0",
|
||||||
"pg": "8.13.1",
|
"pg": "8.13.1",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"stripe": "^22.0.2",
|
"stripe": "^22.0.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
@@ -26,7 +28,9 @@
|
|||||||
"@types/express": "4.17.21",
|
"@types/express": "4.17.21",
|
||||||
"@types/morgan": "1.9.9",
|
"@types/morgan": "1.9.9",
|
||||||
"@types/node": "22.10.2",
|
"@types/node": "22.10.2",
|
||||||
|
"@types/pdfkit": "^0.17.6",
|
||||||
"@types/pg": "8.11.10",
|
"@types/pg": "8.11.10",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"tsx": "4.19.2",
|
"tsx": "4.19.2",
|
||||||
"typescript": "5.7.2"
|
"typescript": "5.7.2"
|
||||||
}
|
}
|
||||||
@@ -523,6 +527,39 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/ciphers": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/helpers": {
|
||||||
|
"version": "0.5.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
|
||||||
|
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
@@ -615,6 +652,16 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pdfkit": {
|
||||||
|
"version": "0.17.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.6.tgz",
|
||||||
|
"integrity": "sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/pg": {
|
"node_modules/@types/pg": {
|
||||||
"version": "8.11.10",
|
"version": "8.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
|
||||||
@@ -627,6 +674,16 @@
|
|||||||
"pg-types": "^4.0.1"
|
"pg-types": "^4.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qrcode": {
|
||||||
|
"version": "1.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
|
||||||
|
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
||||||
@@ -675,12 +732,56 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/basic-auth": {
|
"node_modules/basic-auth": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
@@ -723,6 +824,24 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/browserify-zlib": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "~1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bullmq": {
|
"node_modules/bullmq": {
|
||||||
"version": "5.76.4",
|
"version": "5.76.4",
|
||||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.76.4.tgz",
|
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.76.4.tgz",
|
||||||
@@ -778,6 +897,35 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clone": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cluster-key-slot": {
|
"node_modules/cluster-key-slot": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||||
@@ -787,6 +935,24 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
@@ -857,6 +1023,15 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/denque": {
|
"node_modules/denque": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
@@ -895,6 +1070,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.5",
|
"version": "16.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||||
@@ -927,6 +1114,12 @@
|
|||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
@@ -1082,6 +1275,12 @@
|
|||||||
"express": "^4.11 || 5 || ^5.0.0-beta.1"
|
"express": "^4.11 || 5 || ^5.0.0-beta.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
@@ -1100,6 +1299,36 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fontkit": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.12",
|
||||||
|
"brotli": "^1.3.2",
|
||||||
|
"clone": "^2.1.2",
|
||||||
|
"dfa": "^1.2.0",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"restructure": "^3.0.0",
|
||||||
|
"tiny-inflate": "^1.0.3",
|
||||||
|
"unicode-properties": "^1.4.0",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -1142,6 +1371,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
@@ -1327,6 +1565,52 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-md5": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/linebreak": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "0.0.8",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linebreak/node_modules/base64-js": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
@@ -1570,6 +1854,48 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -1579,12 +1905,35 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdfkit": {
|
||||||
|
"version": "0.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.18.0.tgz",
|
||||||
|
"integrity": "sha512-NvUwSDZ0eYEzqAiWwVQkRkjYUkZ48kcsHuCO31ykqPPIVkwoSDjDGiwIgHHNtsiwls3z3P/zy4q00hl2chg2Ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/ciphers": "^1.0.0",
|
||||||
|
"@noble/hashes": "^1.6.0",
|
||||||
|
"fontkit": "^2.0.4",
|
||||||
|
"js-md5": "^0.8.3",
|
||||||
|
"linebreak": "^1.1.0",
|
||||||
|
"png-js": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pg": {
|
"node_modules/pg": {
|
||||||
"version": "8.13.1",
|
"version": "8.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
|
||||||
@@ -1742,6 +2091,23 @@
|
|||||||
"split2": "^4.1.0"
|
"split2": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/png-js": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"browserify-zlib": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postgres-array": {
|
"node_modules/postgres-array": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
|
||||||
@@ -1805,6 +2171,23 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
@@ -1865,6 +2248,21 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/resolve-pkg-maps": {
|
"node_modules/resolve-pkg-maps": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
@@ -1875,6 +2273,12 @@
|
|||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/restructure": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -1967,6 +2371,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
@@ -2069,6 +2479,32 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stripe": {
|
"node_modules/stripe": {
|
||||||
"version": "22.0.2",
|
"version": "22.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-22.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/stripe/-/stripe-22.0.2.tgz",
|
||||||
@@ -2086,6 +2522,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
@@ -2154,6 +2596,32 @@
|
|||||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
@@ -2181,6 +2649,26 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
@@ -2190,6 +2678,47 @@
|
|||||||
"node": ">=0.4"
|
"node": ">=0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.24.1",
|
"version": "3.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||||
|
|||||||
@@ -23,7 +23,9 @@
|
|||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"morgan": "1.10.0",
|
"morgan": "1.10.0",
|
||||||
"nodemailer": "^8.0.5",
|
"nodemailer": "^8.0.5",
|
||||||
|
"pdfkit": "^0.18.0",
|
||||||
"pg": "8.13.1",
|
"pg": "8.13.1",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"stripe": "^22.0.2",
|
"stripe": "^22.0.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
@@ -32,7 +34,9 @@
|
|||||||
"@types/express": "4.17.21",
|
"@types/express": "4.17.21",
|
||||||
"@types/morgan": "1.9.9",
|
"@types/morgan": "1.9.9",
|
||||||
"@types/node": "22.10.2",
|
"@types/node": "22.10.2",
|
||||||
|
"@types/pdfkit": "^0.17.6",
|
||||||
"@types/pg": "8.11.10",
|
"@types/pg": "8.11.10",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"tsx": "4.19.2",
|
"tsx": "4.19.2",
|
||||||
"typescript": "5.7.2"
|
"typescript": "5.7.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+258
-3
@@ -35,6 +35,7 @@ import {
|
|||||||
completePendingBirdTransfersForOwner,
|
completePendingBirdTransfersForOwner,
|
||||||
createBird,
|
createBird,
|
||||||
createBirdMilestoneReminderDelivery,
|
createBirdMilestoneReminderDelivery,
|
||||||
|
createBirdTransferCode,
|
||||||
createMedicationForBird,
|
createMedicationForBird,
|
||||||
createPendingBirdTransfer,
|
createPendingBirdTransfer,
|
||||||
findBirdsByBandId,
|
findBirdsByBandId,
|
||||||
@@ -45,6 +46,7 @@ import {
|
|||||||
deleteVetVisitForBird,
|
deleteVetVisitForBird,
|
||||||
getBirdById,
|
getBirdById,
|
||||||
getBirdByPublicProfileCode,
|
getBirdByPublicProfileCode,
|
||||||
|
getOpenBirdTransferCode,
|
||||||
listBirds,
|
listBirds,
|
||||||
listDueBirdMilestoneReminders,
|
listDueBirdMilestoneReminders,
|
||||||
listMemorializedBirds,
|
listMemorializedBirds,
|
||||||
@@ -53,6 +55,7 @@ import {
|
|||||||
listVetVisitsForBird,
|
listVetVisitsForBird,
|
||||||
listWeightsForBird,
|
listWeightsForBird,
|
||||||
memorializeBird,
|
memorializeBird,
|
||||||
|
markBirdTransferCodeCompleted,
|
||||||
transferBirdToWorkspace,
|
transferBirdToWorkspace,
|
||||||
updateBird,
|
updateBird,
|
||||||
updateMemorialReminderPreference,
|
updateMemorialReminderPreference,
|
||||||
@@ -61,6 +64,7 @@ import {
|
|||||||
updateVetVisitForBird,
|
updateVetVisitForBird,
|
||||||
} from './repositories/birdRepository.js';
|
} from './repositories/birdRepository.js';
|
||||||
import { createIntegrationTokenRecord, listIntegrationTokens, revokeIntegrationToken } from './repositories/integrationTokenRepository.js';
|
import { createIntegrationTokenRecord, listIntegrationTokens, revokeIntegrationToken } from './repositories/integrationTokenRepository.js';
|
||||||
|
import { renderAdoptionReportPdf } from './reports/adoptionReport.js';
|
||||||
import {
|
import {
|
||||||
createAuditLogEntry,
|
createAuditLogEntry,
|
||||||
createFlockNote,
|
createFlockNote,
|
||||||
@@ -146,6 +150,7 @@ const trustProxy = process.env.TRUST_PROXY?.trim() ?? '';
|
|||||||
const milestoneRemindersEnabled = (process.env.MILESTONE_REMINDERS_ENABLED ?? 'true').toLowerCase() !== 'false';
|
const milestoneRemindersEnabled = (process.env.MILESTONE_REMINDERS_ENABLED ?? 'true').toLowerCase() !== 'false';
|
||||||
const milestoneReminderTimeZone = process.env.MILESTONE_REMINDER_TIME_ZONE?.trim() || 'America/New_York';
|
const milestoneReminderTimeZone = process.env.MILESTONE_REMINDER_TIME_ZONE?.trim() || 'America/New_York';
|
||||||
const milestoneReminderCheckIntervalMs = 60 * 60 * 1000;
|
const milestoneReminderCheckIntervalMs = 60 * 60 * 1000;
|
||||||
|
const adoptionReportWeightHistoryDays = 425;
|
||||||
const photoDeliveryMode = process.env.PHOTO_DELIVERY_MODE === 'redirect' ? 'redirect' : 'proxy';
|
const photoDeliveryMode = process.env.PHOTO_DELIVERY_MODE === 'redirect' ? 'redirect' : 'proxy';
|
||||||
|
|
||||||
if (trustProxy) {
|
if (trustProxy) {
|
||||||
@@ -244,6 +249,7 @@ const lostBirdReportSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const publicProfileCodeSchema = z.string().trim().regex(/^[A-Za-z0-9_-]{8,32}$/);
|
const publicProfileCodeSchema = z.string().trim().regex(/^[A-Za-z0-9_-]{8,32}$/);
|
||||||
|
const birdTransferCodeSchema = z.string().trim().regex(/^[A-Za-z0-9_-]{12,32}$/);
|
||||||
const birdProfileListSchema = z
|
const birdProfileListSchema = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -262,6 +268,10 @@ const birdSchema = z.object({
|
|||||||
motivators: birdProfileListSchema,
|
motivators: birdProfileListSchema,
|
||||||
demotivators: birdProfileListSchema,
|
demotivators: birdProfileListSchema,
|
||||||
favoriteSnack: z.string().trim().max(160).optional().or(z.literal('')),
|
favoriteSnack: z.string().trim().max(160).optional().or(z.literal('')),
|
||||||
|
vetClinicName: z.string().trim().max(160).optional().or(z.literal('')),
|
||||||
|
vetClinicAddress: z.string().trim().max(500).optional().or(z.literal('')),
|
||||||
|
vetAccountNumber: z.string().trim().max(120).optional().or(z.literal('')),
|
||||||
|
vetDoctorName: z.string().trim().max(160).optional().or(z.literal('')),
|
||||||
gender: birdGenderSchema.optional(),
|
gender: birdGenderSchema.optional(),
|
||||||
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
|
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
|
||||||
gotchaDay: dateStringSchema.optional().or(z.literal('')),
|
gotchaDay: dateStringSchema.optional().or(z.literal('')),
|
||||||
@@ -617,6 +627,10 @@ const normalizeBird = (row: BirdRow) => ({
|
|||||||
motivators: row.motivators,
|
motivators: row.motivators,
|
||||||
demotivators: row.demotivators,
|
demotivators: row.demotivators,
|
||||||
favoriteSnack: row.favorite_snack,
|
favoriteSnack: row.favorite_snack,
|
||||||
|
vetClinicName: row.vet_clinic_name,
|
||||||
|
vetClinicAddress: row.vet_clinic_address,
|
||||||
|
vetAccountNumber: row.vet_account_number,
|
||||||
|
vetDoctorName: row.vet_doctor_name,
|
||||||
gender: row.gender,
|
gender: row.gender,
|
||||||
dateOfBirth: row.date_of_birth,
|
dateOfBirth: row.date_of_birth,
|
||||||
gotchaDay: row.gotcha_day,
|
gotchaDay: row.gotcha_day,
|
||||||
@@ -638,6 +652,8 @@ const normalizeBird = (row: BirdRow) => ({
|
|||||||
latestRecordedOn: row.latest_recorded_on,
|
latestRecordedOn: row.latest_recorded_on,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createBirdTransferCodeValue = () => crypto.randomBytes(12).toString('base64url');
|
||||||
|
|
||||||
const normalizePublicBirdProfile = (row: BirdRow) => ({
|
const normalizePublicBirdProfile = (row: BirdRow) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
workspaceId: row.workspace_id,
|
workspaceId: row.workspace_id,
|
||||||
@@ -767,6 +783,7 @@ app.disable('x-powered-by');
|
|||||||
app.use(helmet({ crossOriginResourcePolicy: false }));
|
app.use(helmet({ crossOriginResourcePolicy: false }));
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
|
exposedHeaders: ['X-FlockPal-Transfer-Code'],
|
||||||
origin(origin, callback) {
|
origin(origin, callback) {
|
||||||
if (!origin || allowedOrigins.includes(origin)) {
|
if (!origin || allowedOrigins.includes(origin)) {
|
||||||
callback(null, true);
|
callback(null, true);
|
||||||
@@ -1336,6 +1353,37 @@ const deleteBirdPhotoObjectIfNeeded = async (objectKey: string | null) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadBirdReportPhotoBuffer = async (bird: BirdRow) => {
|
||||||
|
if (!bird.photo_object_key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const s3Config = getS3ImageStorageConfig();
|
||||||
|
|
||||||
|
if (!s3Config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedUrl = getSignedS3ObjectUrl({
|
||||||
|
config: s3Config,
|
||||||
|
objectKey: bird.photo_object_key,
|
||||||
|
expiresInSeconds: 5 * 60,
|
||||||
|
});
|
||||||
|
const imageResponse = await fetch(signedUrl);
|
||||||
|
|
||||||
|
if (!imageResponse.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = imageResponse.headers.get('content-type') || bird.photo_content_type || '';
|
||||||
|
|
||||||
|
if (!/^image\/(?:png|jpe?g)$/i.test(contentType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(await imageResponse.arrayBuffer());
|
||||||
|
};
|
||||||
|
|
||||||
const getDefaultBirdPhotoAttachment = () => {
|
const getDefaultBirdPhotoAttachment = () => {
|
||||||
const defaultPhotoPath = path.join(process.cwd(), 'assets', 'yoda-default.png');
|
const defaultPhotoPath = path.join(process.cwd(), 'assets', 'yoda-default.png');
|
||||||
|
|
||||||
@@ -3118,6 +3166,10 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
|
|||||||
motivators: emptyToNull(parsed.data.motivators),
|
motivators: emptyToNull(parsed.data.motivators),
|
||||||
demotivators: emptyToNull(parsed.data.demotivators),
|
demotivators: emptyToNull(parsed.data.demotivators),
|
||||||
favoriteSnack: emptyToNull(parsed.data.favoriteSnack),
|
favoriteSnack: emptyToNull(parsed.data.favoriteSnack),
|
||||||
|
vetClinicName: emptyToNull(parsed.data.vetClinicName),
|
||||||
|
vetClinicAddress: emptyToNull(parsed.data.vetClinicAddress),
|
||||||
|
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
|
||||||
|
vetDoctorName: emptyToNull(parsed.data.vetDoctorName),
|
||||||
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
|
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
|
||||||
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
|
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
|
||||||
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
||||||
@@ -3142,7 +3194,7 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
|
|||||||
await deleteBirdPhotoObjectIfNeeded(uploadedObjectKeyToCleanup);
|
await deleteBirdPhotoObjectIfNeeded(uploadedObjectKeyToCleanup);
|
||||||
|
|
||||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||||
res.status(409).json({ error: 'That band/tag ID is already in use in this flock.' });
|
res.status(409).json({ error: 'That band/tag ID is already in use in FlockPal.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3224,7 +3276,7 @@ app.post('/api/birds/:birdId/transfer', requireAuth, requireWriteAccess, require
|
|||||||
res.json({ bird: normalizeBird(bird), destinationOwnerEmail, destinationWorkspace: normalizeWorkspace(targetWorkspace) });
|
res.json({ bird: normalizeBird(bird), destinationOwnerEmail, destinationWorkspace: normalizeWorkspace(targetWorkspace) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||||
res.status(409).json({ error: 'That band/tag ID is already in use in the destination flock.' });
|
res.status(409).json({ error: 'That band/tag ID is already in use in FlockPal.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3232,6 +3284,205 @@ app.post('/api/birds/:birdId/transfer', requireAuth, requireWriteAccess, require
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/api/birds/:birdId/transfer-code',
|
||||||
|
requireAuth,
|
||||||
|
requireWriteAccess,
|
||||||
|
requireSessionAuth,
|
||||||
|
requireWorkspaceRole(['owner', 'assistant']),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const sourceBird = await getBirdById(req.params.birdId, req.auth!.workspace.id);
|
||||||
|
|
||||||
|
if (!sourceBird) {
|
||||||
|
res.status(404).json({ error: 'Bird not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ensureBirdWritable(sourceBird, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let transferCode = null;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
|
try {
|
||||||
|
transferCode = await createBirdTransferCode({
|
||||||
|
code: createBirdTransferCodeValue(),
|
||||||
|
birdId: sourceBird.id,
|
||||||
|
sourceWorkspaceId: req.auth!.workspace.id,
|
||||||
|
requestedByUserId: req.auth!.user.id,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof error === 'object' && error && 'code' in error && error.code === '23505' && attempt < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transferCode) {
|
||||||
|
throw new Error('Unable to create bird transfer code.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeAuditLog(req.auth!, 'bird.transfer_code_created', 'bird', sourceBird.id, sourceBird.name, {
|
||||||
|
transferCodeId: transferCode.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
transferCode: {
|
||||||
|
code: transferCode.code,
|
||||||
|
bird: normalizeBird(sourceBird),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/api/bird-transfer-codes/:code/accept',
|
||||||
|
requireAuth,
|
||||||
|
requireWriteAccess,
|
||||||
|
requireSessionAuth,
|
||||||
|
requireWorkspaceRole(['owner', 'assistant']),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const parsed = birdTransferCodeSchema.safeParse(req.params.code);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(404).json({ error: 'Bird transfer code not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transferCode = await getOpenBirdTransferCode(parsed.data);
|
||||||
|
|
||||||
|
if (!transferCode) {
|
||||||
|
res.status(404).json({ error: 'Bird transfer code not found or already used.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transferCode.source_workspace_id === req.auth!.workspace.id) {
|
||||||
|
res.status(409).json({ error: 'This bird is already in your active flock.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bird = await transferBirdToWorkspace(transferCode.id, transferCode.source_workspace_id, req.auth!.workspace.id);
|
||||||
|
|
||||||
|
if (!bird) {
|
||||||
|
res.status(404).json({ error: 'Bird is no longer available for transfer.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await markBirdTransferCodeCompleted(transferCode.transfer_code_id, req.auth!.workspace.id);
|
||||||
|
await writeAuditLog(req.auth!, 'bird.transfer_code_accepted', 'bird', bird.id, bird.name, {
|
||||||
|
sourceWorkspaceId: transferCode.source_workspace_id,
|
||||||
|
sourceWorkspaceName: transferCode.workspace_name,
|
||||||
|
transferCodeId: transferCode.transfer_code_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ bird: normalizeBird(bird), sourceWorkspaceName: transferCode.workspace_name, workspace: normalizeWorkspace(req.auth!.workspace) });
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||||
|
res.status(409).json({ error: 'That band/tag ID is already in use in FlockPal.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/api/birds/:birdId/reports/adoption',
|
||||||
|
requireAuth,
|
||||||
|
requireWriteAccess,
|
||||||
|
requireSessionAuth,
|
||||||
|
requireWorkspaceRole(['owner', 'assistant']),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const sourceBird = await getBirdById(req.params.birdId, req.auth!.workspace.id);
|
||||||
|
|
||||||
|
if (!sourceBird) {
|
||||||
|
res.status(404).json({ error: 'Bird not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ensureBirdWritable(sourceBird, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let transferCode = null;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
|
try {
|
||||||
|
transferCode = await createBirdTransferCode({
|
||||||
|
code: createBirdTransferCodeValue(),
|
||||||
|
birdId: sourceBird.id,
|
||||||
|
sourceWorkspaceId: req.auth!.workspace.id,
|
||||||
|
requestedByUserId: req.auth!.user.id,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof error === 'object' && error && 'code' in error && error.code === '23505' && attempt < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transferCode) {
|
||||||
|
throw new Error('Unable to create bird transfer code.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [weights, vetVisits, notes, birdPhotoBuffer] = await Promise.all([
|
||||||
|
listWeightsForBird(sourceBird.id, req.auth!.workspace.id, adoptionReportWeightHistoryDays),
|
||||||
|
listVetVisitsForBird(sourceBird.id, req.auth!.workspace.id),
|
||||||
|
listFlockNotes(req.auth!.workspace.id),
|
||||||
|
loadBirdReportPhotoBuffer(sourceBird),
|
||||||
|
]);
|
||||||
|
const birdNotes = notes.filter((note) => note.bird_id === sourceBird.id);
|
||||||
|
const pdf = await renderAdoptionReportPdf({
|
||||||
|
bird: sourceBird,
|
||||||
|
weights,
|
||||||
|
vetVisits,
|
||||||
|
notes: birdNotes,
|
||||||
|
transferCode: transferCode.code,
|
||||||
|
birdPhotoBuffer,
|
||||||
|
printFriendly: req.query.printFriendly === 'true',
|
||||||
|
assets: {
|
||||||
|
logoPath: path.join(process.cwd(), 'assets', 'flockpal-logo.png'),
|
||||||
|
wordmarkPath: path.join(process.cwd(), 'assets', 'flockpal-text.png'),
|
||||||
|
defaultBirdPhotoPath: path.join(process.cwd(), 'assets', 'yoda-default.png'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeAuditLog(req.auth!, 'bird.adoption_report_created', 'bird', sourceBird.id, sourceBird.name, {
|
||||||
|
transferCodeId: transferCode.id,
|
||||||
|
printFriendly: req.query.printFriendly === 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
const safeName = sourceBird.name
|
||||||
|
.trim()
|
||||||
|
.replace(/[^a-z0-9]+/gi, '-')
|
||||||
|
.replace(/^-+|-+$/g, '')
|
||||||
|
.toLowerCase() || 'bird';
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/pdf');
|
||||||
|
res.setHeader('Content-Disposition', `inline; filename="flockpal-adoption-report-${safeName}.pdf"`);
|
||||||
|
res.setHeader('Content-Length', pdf.length.toString());
|
||||||
|
res.setHeader('X-FlockPal-Transfer-Code', transferCode.code);
|
||||||
|
res.send(pdf);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceRole(['owner', 'assistant', 'caregiver']), async (req: Request, res: Response, next: NextFunction) => {
|
app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceRole(['owner', 'assistant', 'caregiver']), async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const parsed = birdSchema.safeParse(req.body);
|
const parsed = birdSchema.safeParse(req.body);
|
||||||
|
|
||||||
@@ -3271,6 +3522,10 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
|
|||||||
motivators: emptyToNull(parsed.data.motivators),
|
motivators: emptyToNull(parsed.data.motivators),
|
||||||
demotivators: emptyToNull(parsed.data.demotivators),
|
demotivators: emptyToNull(parsed.data.demotivators),
|
||||||
favoriteSnack: emptyToNull(parsed.data.favoriteSnack),
|
favoriteSnack: emptyToNull(parsed.data.favoriteSnack),
|
||||||
|
vetClinicName: emptyToNull(parsed.data.vetClinicName),
|
||||||
|
vetClinicAddress: emptyToNull(parsed.data.vetClinicAddress),
|
||||||
|
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
|
||||||
|
vetDoctorName: emptyToNull(parsed.data.vetDoctorName),
|
||||||
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
|
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
|
||||||
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
|
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
|
||||||
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
||||||
@@ -3301,7 +3556,7 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
|
|||||||
await deleteBirdPhotoObjectIfNeeded(uploadedObjectKeyToCleanup);
|
await deleteBirdPhotoObjectIfNeeded(uploadedObjectKeyToCleanup);
|
||||||
|
|
||||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||||
res.status(409).json({ error: 'That band/tag ID is already in use in this flock.' });
|
res.status(409).json({ error: 'That band/tag ID is already in use in FlockPal.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
|||||||
motivators VARCHAR(1000),
|
motivators VARCHAR(1000),
|
||||||
demotivators VARCHAR(1000),
|
demotivators VARCHAR(1000),
|
||||||
favorite_snack VARCHAR(160),
|
favorite_snack VARCHAR(160),
|
||||||
|
vet_clinic_name VARCHAR(160),
|
||||||
|
vet_clinic_address VARCHAR(500),
|
||||||
|
vet_account_number VARCHAR(120),
|
||||||
|
vet_doctor_name VARCHAR(160),
|
||||||
gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
|
gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
|
||||||
date_of_birth DATE,
|
date_of_birth DATE,
|
||||||
gotcha_day DATE,
|
gotcha_day DATE,
|
||||||
@@ -239,6 +243,10 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
|||||||
ADD COLUMN IF NOT EXISTS motivators VARCHAR(1000),
|
ADD COLUMN IF NOT EXISTS motivators VARCHAR(1000),
|
||||||
ADD COLUMN IF NOT EXISTS demotivators VARCHAR(1000),
|
ADD COLUMN IF NOT EXISTS demotivators VARCHAR(1000),
|
||||||
ADD COLUMN IF NOT EXISTS favorite_snack VARCHAR(160),
|
ADD COLUMN IF NOT EXISTS favorite_snack VARCHAR(160),
|
||||||
|
ADD COLUMN IF NOT EXISTS vet_clinic_name VARCHAR(160),
|
||||||
|
ADD COLUMN IF NOT EXISTS vet_clinic_address VARCHAR(500),
|
||||||
|
ADD COLUMN IF NOT EXISTS vet_account_number VARCHAR(120),
|
||||||
|
ADD COLUMN IF NOT EXISTS vet_doctor_name VARCHAR(160),
|
||||||
ADD COLUMN IF NOT EXISTS gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
|
ADD COLUMN IF NOT EXISTS gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
|
||||||
ADD COLUMN IF NOT EXISTS date_of_birth DATE,
|
ADD COLUMN IF NOT EXISTS date_of_birth DATE,
|
||||||
ADD COLUMN IF NOT EXISTS gotcha_day DATE,
|
ADD COLUMN IF NOT EXISTS gotcha_day DATE,
|
||||||
@@ -284,8 +292,8 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
|||||||
|
|
||||||
DROP INDEX IF EXISTS idx_birds_workspace_tag_id;
|
DROP INDEX IF EXISTS idx_birds_workspace_tag_id;
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_birds_workspace_tag_id
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_birds_global_tag_id
|
||||||
ON birds (workspace_id, LOWER(tag_id))
|
ON birds (LOWER(BTRIM(tag_id)))
|
||||||
WHERE tag_id IS NOT NULL
|
WHERE tag_id IS NOT NULL
|
||||||
AND BTRIM(tag_id) <> ''
|
AND BTRIM(tag_id) <> ''
|
||||||
AND LOWER(BTRIM(tag_id)) NOT IN ('unknown', 'not recorded', 'n/a', 'na', 'none');
|
AND LOWER(BTRIM(tag_id)) NOT IN ('unknown', 'not recorded', 'n/a', 'na', 'none');
|
||||||
@@ -338,6 +346,28 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
|||||||
ON pending_bird_transfers (bird_id)
|
ON pending_bird_transfers (bird_id)
|
||||||
WHERE completed_at IS NULL;
|
WHERE completed_at IS NULL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bird_transfer_codes (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
code VARCHAR(32) NOT NULL UNIQUE,
|
||||||
|
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
||||||
|
source_workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||||
|
requested_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
completed_workspace_id INTEGER REFERENCES workspaces(id) ON DELETE SET NULL,
|
||||||
|
revoked_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bird_transfer_codes_open_bird
|
||||||
|
ON bird_transfer_codes (bird_id, created_at DESC)
|
||||||
|
WHERE completed_at IS NULL
|
||||||
|
AND revoked_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bird_transfer_codes_code_open
|
||||||
|
ON bird_transfer_codes (code)
|
||||||
|
WHERE completed_at IS NULL
|
||||||
|
AND revoked_at IS NULL;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS flock_notes (
|
CREATE TABLE IF NOT EXISTS flock_notes (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import PDFDocument from 'pdfkit';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
|
||||||
|
import type { BirdRow, FlockNoteRow, VetVisitRow, WeightRow } from '../types.js';
|
||||||
|
|
||||||
|
type AdoptionReportInput = {
|
||||||
|
bird: BirdRow;
|
||||||
|
weights: WeightRow[];
|
||||||
|
vetVisits: VetVisitRow[];
|
||||||
|
notes: FlockNoteRow[];
|
||||||
|
transferCode: string;
|
||||||
|
birdPhotoBuffer?: Buffer | null;
|
||||||
|
assets: {
|
||||||
|
logoPath: string;
|
||||||
|
wordmarkPath: string;
|
||||||
|
defaultBirdPhotoPath: string;
|
||||||
|
};
|
||||||
|
printFriendly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const page = { width: 612, height: 792, margin: 42 };
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
ink: '#1f2a2a',
|
||||||
|
muted: '#5d5f59',
|
||||||
|
red: '#cb3a35',
|
||||||
|
green: '#238a5a',
|
||||||
|
blue: '#2769b3',
|
||||||
|
border: '#cfe0d5',
|
||||||
|
panel: '#fbf7ee',
|
||||||
|
paper: '#fffdf9',
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (value: string | null) => {
|
||||||
|
if (!value) {
|
||||||
|
return 'Not recorded';
|
||||||
|
}
|
||||||
|
return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }).format(
|
||||||
|
new Date(`${value.slice(0, 10)}T00:00:00Z`),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (value: string | null) => {
|
||||||
|
if (!value) {
|
||||||
|
return 'Not recorded';
|
||||||
|
}
|
||||||
|
return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatShortDate = (value: string | null) => {
|
||||||
|
if (!value) {
|
||||||
|
return 'No data yet';
|
||||||
|
}
|
||||||
|
return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' }).format(new Date(`${value.slice(0, 10)}T00:00:00Z`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatWeight = (value: string | number | null) => {
|
||||||
|
const numericValue = value === null ? null : Number(value);
|
||||||
|
return numericValue && Number.isFinite(numericValue) ? `${numericValue.toFixed(1)} g` : 'Pending';
|
||||||
|
};
|
||||||
|
|
||||||
|
const genderLabel = (value: string) => {
|
||||||
|
if (value === 'female') {
|
||||||
|
return 'Female';
|
||||||
|
}
|
||||||
|
if (value === 'male') {
|
||||||
|
return 'Male';
|
||||||
|
}
|
||||||
|
return 'Unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseList = (value: string | null) =>
|
||||||
|
(value ?? '')
|
||||||
|
.split(/\r?\n|,/)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const dataUrlToBuffer = (value: string | null) => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = value.match(/^data:image\/(?:png|jpeg|jpg);base64,(.+)$/);
|
||||||
|
return match ? Buffer.from(match[1], 'base64') : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectPdf = (doc: PDFKit.PDFDocument) =>
|
||||||
|
new Promise<Buffer>((resolve, reject) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
doc.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
||||||
|
doc.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fitText = (doc: PDFKit.PDFDocument, text: string, x: number, y: number, width: number, options: PDFKit.Mixins.TextOptions = {}) => {
|
||||||
|
doc.text(text, x, y, { width, lineGap: 1.5, ...options });
|
||||||
|
return doc.y;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawFact = (doc: PDFKit.PDFDocument, label: string, value: string, x: number, y: number, width: number) => {
|
||||||
|
doc.roundedRect(x, y, width, 43, 6).fillAndStroke(colors.panel, colors.border);
|
||||||
|
doc.fillColor(colors.muted).fontSize(7).font('Helvetica-Bold').text(label.toUpperCase(), x + 8, y + 8, { width: width - 16 });
|
||||||
|
doc.fillColor(colors.ink).fontSize(10).font('Helvetica-Bold').text(value, x + 8, y + 21, { width: width - 16, ellipsis: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawTextCard = (doc: PDFKit.PDFDocument, label: string, value: string, x: number, y: number, width: number, height = 58) => {
|
||||||
|
doc.roundedRect(x, y, width, height, 6).fillAndStroke(colors.panel, colors.border);
|
||||||
|
doc.fillColor(colors.blue).fontSize(8).font('Helvetica-Bold').text(label.toUpperCase(), x + 8, y + 8, { width: width - 16 });
|
||||||
|
doc.fillColor(colors.ink).fontSize(9.2).font('Helvetica').text(value, x + 8, y + 23, {
|
||||||
|
width: width - 16,
|
||||||
|
height: height - 31,
|
||||||
|
ellipsis: true,
|
||||||
|
lineGap: 1.2,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawSectionTitle = (doc: PDFKit.PDFDocument, title: string, y: number) => {
|
||||||
|
doc.fillColor(colors.green).font('Helvetica-Bold').fontSize(14).text(title, page.margin, y);
|
||||||
|
doc.moveTo(page.margin, y + 19).lineTo(page.width - page.margin, y + 19).strokeColor(colors.border).lineWidth(1).stroke();
|
||||||
|
return y + 27;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawSimpleWeightChart = (doc: PDFKit.PDFDocument, weights: WeightRow[], birdColor: string, x: number, y: number, width: number, height: number) => {
|
||||||
|
const plottedWeights = weights
|
||||||
|
.slice()
|
||||||
|
.sort((left, right) => left.recorded_on.localeCompare(right.recorded_on))
|
||||||
|
.map((entry) => ({ ...entry, numericWeight: Number(entry.weight_grams) }))
|
||||||
|
.filter((entry) => Number.isFinite(entry.numericWeight));
|
||||||
|
|
||||||
|
doc.roundedRect(x, y, width, height, 8).fillAndStroke('#fffdf9', colors.border);
|
||||||
|
|
||||||
|
if (!plottedWeights.length) {
|
||||||
|
doc.fillColor(colors.muted).fontSize(10).text('Add more weight records to show a trend graph.', x + 14, y + height / 2 - 6, {
|
||||||
|
width: width - 28,
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestDate = new Date(`${plottedWeights[plottedWeights.length - 1].recorded_on.slice(0, 10)}T00:00:00Z`);
|
||||||
|
const startDate = new Date(latestDate);
|
||||||
|
startDate.setUTCDate(startDate.getUTCDate() - 29);
|
||||||
|
const visibleWeights = plottedWeights.filter((entry) => {
|
||||||
|
const recordedOn = new Date(`${entry.recorded_on.slice(0, 10)}T00:00:00Z`);
|
||||||
|
return recordedOn >= startDate && recordedOn <= latestDate;
|
||||||
|
});
|
||||||
|
const rawMinWeight = Math.min(...visibleWeights.map((entry) => entry.numericWeight));
|
||||||
|
const rawMaxWeight = Math.max(...visibleWeights.map((entry) => entry.numericWeight));
|
||||||
|
const rangePadding = Math.max((rawMaxWeight - rawMinWeight) * 0.12, 2);
|
||||||
|
const minWeight = Math.max(0, rawMinWeight - rangePadding);
|
||||||
|
const maxWeight = rawMaxWeight + rangePadding;
|
||||||
|
const weightRange = Math.max(1, maxWeight - minWeight);
|
||||||
|
const padding = { top: 16, right: 18, bottom: 32, left: 48 };
|
||||||
|
const plotWidth = width - padding.left - padding.right;
|
||||||
|
const plotHeight = height - padding.top - padding.bottom;
|
||||||
|
const startMs = startDate.getTime();
|
||||||
|
const endMs = latestDate.getTime();
|
||||||
|
const dateRange = Math.max(endMs - startMs, 24 * 60 * 60 * 1000);
|
||||||
|
const chartColor = /^#[0-9a-fA-F]{6}$/.test(birdColor) ? birdColor : colors.green;
|
||||||
|
const midWeight = minWeight + (maxWeight - minWeight) / 2;
|
||||||
|
const midDate = new Date((startMs + endMs) / 2);
|
||||||
|
const yTicks = [
|
||||||
|
{ label: `${maxWeight.toFixed(0)} g`, y: y + padding.top },
|
||||||
|
{ label: `${midWeight.toFixed(0)} g`, y: y + padding.top + plotHeight / 2 },
|
||||||
|
{ label: `${minWeight.toFixed(0)} g`, y: y + padding.top + plotHeight },
|
||||||
|
];
|
||||||
|
const xTicks = [
|
||||||
|
{ label: formatShortDate(startDate.toISOString().slice(0, 10)), x: x + padding.left },
|
||||||
|
{ label: formatShortDate(midDate.toISOString().slice(0, 10)), x: x + padding.left + plotWidth / 2 },
|
||||||
|
{ label: formatShortDate(latestDate.toISOString().slice(0, 10)), x: x + padding.left + plotWidth },
|
||||||
|
];
|
||||||
|
|
||||||
|
const points = visibleWeights.map((entry) => {
|
||||||
|
const recordedOn = new Date(`${entry.recorded_on.slice(0, 10)}T00:00:00Z`);
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
x: x + padding.left + ((recordedOn.getTime() - startMs) / dateRange) * plotWidth,
|
||||||
|
y: y + padding.top + (1 - (entry.numericWeight - minWeight) / weightRange) * plotHeight,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.font('Helvetica').fontSize(7).fillColor(colors.muted);
|
||||||
|
yTicks.forEach((tick) => {
|
||||||
|
doc.text(tick.label, x + 4, tick.y - 3, { width: padding.left - 12, align: 'right' });
|
||||||
|
doc
|
||||||
|
.save()
|
||||||
|
.dash(4, { space: 6 })
|
||||||
|
.strokeColor('#d8e5ef')
|
||||||
|
.lineWidth(0.8)
|
||||||
|
.moveTo(x + padding.left, tick.y)
|
||||||
|
.lineTo(x + width - padding.right, tick.y)
|
||||||
|
.stroke()
|
||||||
|
.restore();
|
||||||
|
});
|
||||||
|
doc.strokeColor('#c7cdca').lineWidth(1).moveTo(x + padding.left, y + padding.top + plotHeight).lineTo(x + width - padding.right, y + padding.top + plotHeight).stroke();
|
||||||
|
xTicks.forEach((tick) => {
|
||||||
|
doc.fillColor(colors.muted).fontSize(7).text(tick.label, tick.x - 28, y + height - 18, { width: 56, align: 'center' });
|
||||||
|
});
|
||||||
|
|
||||||
|
points.forEach((entry, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
doc.moveTo(entry.x, entry.y);
|
||||||
|
} else {
|
||||||
|
doc.lineTo(entry.x, entry.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (points.length > 1) {
|
||||||
|
doc.lineCap('round').strokeColor(chartColor).lineWidth(2.4).stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
points.forEach((entry) => {
|
||||||
|
doc.circle(entry.x, entry.y, 3.5).fillAndStroke(chartColor, '#fffdf9');
|
||||||
|
});
|
||||||
|
|
||||||
|
const latestPoint = points[points.length - 1];
|
||||||
|
const calloutOnLeft = latestPoint.x > x + width - padding.right - 84;
|
||||||
|
const calloutX = calloutOnLeft ? latestPoint.x - 82 : latestPoint.x + 8;
|
||||||
|
const calloutY = latestPoint.y < y + padding.top + 18 ? latestPoint.y + 8 : latestPoint.y - 22;
|
||||||
|
doc.roundedRect(calloutX, calloutY, 74, 18, 5).fillAndStroke('#fffdf9', '#d9dedb');
|
||||||
|
doc.fillColor(colors.ink).font('Helvetica-Bold').fontSize(7.5).text(`Latest ${formatWeight(latestPoint.numericWeight)}`, calloutX + 5, calloutY + 5, {
|
||||||
|
width: 64,
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawTable = (doc: PDFKit.PDFDocument, headers: string[], rows: string[][], x: number, y: number, widths: number[], rowHeight = 28) => {
|
||||||
|
doc.font('Helvetica-Bold').fontSize(8).fillColor(colors.muted);
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
doc.text(header.toUpperCase(), x + widths.slice(0, index).reduce((sum, value) => sum + value, 0), y, { width: widths[index] - 8 });
|
||||||
|
});
|
||||||
|
y += 15;
|
||||||
|
doc.moveTo(x, y - 4).lineTo(x + widths.reduce((sum, value) => sum + value, 0), y - 4).strokeColor(colors.border).stroke();
|
||||||
|
|
||||||
|
doc.font('Helvetica').fontSize(8.5).fillColor(colors.ink);
|
||||||
|
rows.forEach((row) => {
|
||||||
|
if (y + rowHeight > page.height - page.margin) {
|
||||||
|
doc.addPage();
|
||||||
|
y = page.margin;
|
||||||
|
}
|
||||||
|
row.forEach((value, index) => {
|
||||||
|
doc.text(value, x + widths.slice(0, index).reduce((sum, columnWidth) => sum + columnWidth, 0), y, {
|
||||||
|
width: widths[index] - 8,
|
||||||
|
height: rowHeight - 6,
|
||||||
|
ellipsis: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
y += rowHeight;
|
||||||
|
doc.moveTo(x, y - 4).lineTo(x + widths.reduce((sum, value) => sum + value, 0), y - 4).strokeColor(colors.border).stroke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return y + 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderAdoptionReportPdf = async ({
|
||||||
|
bird,
|
||||||
|
weights,
|
||||||
|
vetVisits,
|
||||||
|
notes,
|
||||||
|
transferCode,
|
||||||
|
birdPhotoBuffer = null,
|
||||||
|
assets,
|
||||||
|
printFriendly = false,
|
||||||
|
}: AdoptionReportInput) => {
|
||||||
|
const doc = new PDFDocument({
|
||||||
|
size: 'LETTER',
|
||||||
|
margin: page.margin,
|
||||||
|
info: { Title: `FlockPal Adoption Report - ${bird.name}`, Author: 'FlockPal', Subject: `Adoption report for ${bird.name}` },
|
||||||
|
});
|
||||||
|
const output = collectPdf(doc);
|
||||||
|
|
||||||
|
if (!printFriendly) {
|
||||||
|
doc.rect(0, 0, page.width, page.height).fill(colors.paper);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoPath = fs.existsSync(assets.logoPath) ? assets.logoPath : null;
|
||||||
|
const wordmarkPath = fs.existsSync(assets.wordmarkPath) ? assets.wordmarkPath : logoPath;
|
||||||
|
const defaultPhotoPath = fs.existsSync(assets.defaultBirdPhotoPath) ? assets.defaultBirdPhotoPath : null;
|
||||||
|
const photoBuffer = birdPhotoBuffer ?? dataUrlToBuffer(bird.photo_data_url);
|
||||||
|
const contentWidth = page.width - page.margin * 2;
|
||||||
|
const headerY = page.margin;
|
||||||
|
const headerHeight = 136;
|
||||||
|
|
||||||
|
doc.roundedRect(page.margin, headerY, contentWidth, headerHeight, 12).fillAndStroke(printFriendly ? '#ffffff' : '#f8f4e8', colors.border);
|
||||||
|
if (logoPath) {
|
||||||
|
doc.image(logoPath, page.margin + 10, headerY + 18, { fit: [92, 84], align: 'center', valign: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const photoX = page.margin + 235;
|
||||||
|
const photoY = headerY + 13;
|
||||||
|
if (photoBuffer) {
|
||||||
|
doc.image(photoBuffer, photoX, photoY, { fit: [58, 58], align: 'center', valign: 'center' });
|
||||||
|
} else if (defaultPhotoPath) {
|
||||||
|
doc.image(defaultPhotoPath, photoX, photoY, { fit: [58, 58], align: 'center', valign: 'center' });
|
||||||
|
}
|
||||||
|
doc.roundedRect(photoX, photoY, 58, 58, 10).strokeColor('#ffffff').lineWidth(2).stroke();
|
||||||
|
doc.fillColor(colors.red).font('Helvetica-Bold').fontSize(22).text(bird.name, page.margin + 140, headerY + 75, { width: 250, align: 'center' });
|
||||||
|
doc.fillColor(colors.muted).font('Helvetica').fontSize(9).text('Adoption Report', page.margin + 140, headerY + 98, { width: 250, align: 'center' });
|
||||||
|
|
||||||
|
const qrDataUrl = await QRCode.toDataURL(transferCode, { margin: 1, width: 96, errorCorrectionLevel: 'H' });
|
||||||
|
const qrBuffer = dataUrlToBuffer(qrDataUrl);
|
||||||
|
const qrX = page.width - page.margin - 132;
|
||||||
|
const qrWidth = 124;
|
||||||
|
doc.fillColor(colors.green).font('Helvetica-Bold').fontSize(8).text('JOIN', qrX, headerY + 7, { width: qrWidth, align: 'center' });
|
||||||
|
if (wordmarkPath) {
|
||||||
|
doc.image(wordmarkPath, qrX + 7, headerY + 18, { fit: [110, 34], align: 'center', valign: 'center' });
|
||||||
|
}
|
||||||
|
doc.fillColor(colors.red).font('Helvetica-Bold').fontSize(7.5).text('Keep my story growing', qrX, headerY + 51, {
|
||||||
|
width: qrWidth,
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
|
if (qrBuffer) {
|
||||||
|
doc.image(qrBuffer, qrX + 37, headerY + 62, { width: 50 });
|
||||||
|
}
|
||||||
|
doc.fillColor(colors.green).font('Helvetica-Bold').fontSize(6.8).text('Scan to continue tracking in FlockPal', qrX, headerY + 114, {
|
||||||
|
width: qrWidth,
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
|
doc.fillColor(colors.ink).font('Helvetica').fontSize(6.5).text(transferCode, qrX, headerY + 126, { width: qrWidth, align: 'center' });
|
||||||
|
|
||||||
|
let y = headerY + headerHeight + 16;
|
||||||
|
const factGap = 8;
|
||||||
|
const factWidth = (contentWidth - factGap) / 2;
|
||||||
|
const facts = [
|
||||||
|
['Species', bird.species],
|
||||||
|
['Band/tag ID', bird.tag_id || 'Not recorded'],
|
||||||
|
['Sex', genderLabel(bird.gender)],
|
||||||
|
['Hatch day', formatDate(bird.date_of_birth)],
|
||||||
|
['Favorite snack', bird.favorite_snack || 'Not recorded'],
|
||||||
|
['Latest weight', bird.latest_weight_grams ? `${formatWeight(bird.latest_weight_grams)}${bird.latest_recorded_on ? ` on ${formatDate(bird.latest_recorded_on)}` : ''}` : 'Pending'],
|
||||||
|
];
|
||||||
|
facts.forEach(([label, value], index) => {
|
||||||
|
drawFact(doc, label, value, page.margin + (index % 2) * (factWidth + factGap), y + Math.floor(index / 2) * 50, factWidth);
|
||||||
|
});
|
||||||
|
y += Math.ceil(facts.length / 2) * 50 + 8;
|
||||||
|
|
||||||
|
const motivators = parseList(bird.motivators);
|
||||||
|
const demotivators = parseList(bird.demotivators);
|
||||||
|
drawTextCard(doc, 'Motivators', motivators.length ? motivators.join(', ') : 'Not recorded', page.margin, y, factWidth);
|
||||||
|
drawTextCard(
|
||||||
|
doc,
|
||||||
|
'Demotivators',
|
||||||
|
demotivators.length ? demotivators.join(', ') : 'Not recorded',
|
||||||
|
page.margin + factWidth + factGap,
|
||||||
|
y,
|
||||||
|
factWidth,
|
||||||
|
);
|
||||||
|
y += 72;
|
||||||
|
|
||||||
|
if (y > 610) {
|
||||||
|
doc.addPage();
|
||||||
|
y = page.margin;
|
||||||
|
}
|
||||||
|
y = drawSectionTitle(doc, 'Veterinary Clinic Info', y);
|
||||||
|
const vetFacts = [
|
||||||
|
['Clinic name', bird.vet_clinic_name || 'Not recorded'],
|
||||||
|
['Clinic address', bird.vet_clinic_address || 'Not recorded'],
|
||||||
|
['Account #', bird.vet_account_number || 'Not recorded'],
|
||||||
|
['Dr. name', bird.vet_doctor_name || 'Not recorded'],
|
||||||
|
];
|
||||||
|
vetFacts.forEach(([label, value], index) => {
|
||||||
|
drawFact(doc, label, value, page.margin + (index % 2) * (factWidth + factGap), y + Math.floor(index / 2) * 50, factWidth);
|
||||||
|
});
|
||||||
|
y += Math.ceil(vetFacts.length / 2) * 50 + 8;
|
||||||
|
|
||||||
|
y = drawSectionTitle(doc, 'Vet Visit History', y);
|
||||||
|
y = drawTable(
|
||||||
|
doc,
|
||||||
|
['Date', 'Clinic', 'Reason', 'Notes'],
|
||||||
|
vetVisits.length ? vetVisits.map((visit) => [formatDate(visit.visited_on), visit.clinic_name, visit.reason, visit.notes || '']) : [['No vet visits recorded.', '', '', '']],
|
||||||
|
page.margin,
|
||||||
|
y,
|
||||||
|
[70, 115, 120, contentWidth - 305],
|
||||||
|
28,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (y > 575) {
|
||||||
|
doc.addPage();
|
||||||
|
y = page.margin;
|
||||||
|
}
|
||||||
|
y = drawSectionTitle(doc, 'Weight Graph', y);
|
||||||
|
drawSimpleWeightChart(doc, weights, bird.chart_color, page.margin, y, contentWidth, 120);
|
||||||
|
y += 140;
|
||||||
|
|
||||||
|
y = drawSectionTitle(doc, 'Weight History', y);
|
||||||
|
y = drawTable(
|
||||||
|
doc,
|
||||||
|
['Date', 'Weight', 'Notes'],
|
||||||
|
weights.length ? weights.map((entry) => [formatDate(entry.recorded_on), formatWeight(entry.weight_grams), entry.notes || '']) : [['No weights recorded.', '', '']],
|
||||||
|
page.margin,
|
||||||
|
y,
|
||||||
|
[95, 70, contentWidth - 165],
|
||||||
|
24,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (notes.length) {
|
||||||
|
if (y > 635) {
|
||||||
|
doc.addPage();
|
||||||
|
y = page.margin;
|
||||||
|
}
|
||||||
|
y = drawSectionTitle(doc, 'Notes', y);
|
||||||
|
notes.slice(0, 8).forEach((note) => {
|
||||||
|
if (y > page.height - page.margin - 48) {
|
||||||
|
doc.addPage();
|
||||||
|
y = page.margin;
|
||||||
|
}
|
||||||
|
doc.fillColor(colors.muted).font('Helvetica-Bold').fontSize(8).text(formatDateTime(note.updated_at), page.margin, y);
|
||||||
|
y = fitText(doc, note.body, page.margin, y + 12, contentWidth, { height: 44, ellipsis: true });
|
||||||
|
y += 8;
|
||||||
|
doc.moveTo(page.margin, y).lineTo(page.width - page.margin, y).strokeColor(colors.border).stroke();
|
||||||
|
y += 8;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.end();
|
||||||
|
return output;
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
BirdMilestoneReminderDeliveryRow,
|
BirdMilestoneReminderDeliveryRow,
|
||||||
BirdMilestoneReminderType,
|
BirdMilestoneReminderType,
|
||||||
BirdRow,
|
BirdRow,
|
||||||
|
BirdTransferCodeRow,
|
||||||
LostBirdMatchRow,
|
LostBirdMatchRow,
|
||||||
MedicationAdministrationRow,
|
MedicationAdministrationRow,
|
||||||
MedicationDoseScheduleItem,
|
MedicationDoseScheduleItem,
|
||||||
@@ -23,6 +24,10 @@ const birdSelectFields = `
|
|||||||
birds.motivators,
|
birds.motivators,
|
||||||
birds.demotivators,
|
birds.demotivators,
|
||||||
birds.favorite_snack,
|
birds.favorite_snack,
|
||||||
|
birds.vet_clinic_name,
|
||||||
|
birds.vet_clinic_address,
|
||||||
|
birds.vet_account_number,
|
||||||
|
birds.vet_doctor_name,
|
||||||
birds.gender,
|
birds.gender,
|
||||||
birds.date_of_birth::text,
|
birds.date_of_birth::text,
|
||||||
birds.gotcha_day::text,
|
birds.gotcha_day::text,
|
||||||
@@ -287,6 +292,10 @@ export const createBird = async ({
|
|||||||
motivators,
|
motivators,
|
||||||
demotivators,
|
demotivators,
|
||||||
favoriteSnack,
|
favoriteSnack,
|
||||||
|
vetClinicName = null,
|
||||||
|
vetClinicAddress = null,
|
||||||
|
vetAccountNumber = null,
|
||||||
|
vetDoctorName = null,
|
||||||
gender,
|
gender,
|
||||||
dateOfBirth,
|
dateOfBirth,
|
||||||
gotchaDay,
|
gotchaDay,
|
||||||
@@ -308,6 +317,10 @@ export const createBird = async ({
|
|||||||
motivators: string | null;
|
motivators: string | null;
|
||||||
demotivators: string | null;
|
demotivators: string | null;
|
||||||
favoriteSnack: string | null;
|
favoriteSnack: string | null;
|
||||||
|
vetClinicName?: string | null;
|
||||||
|
vetClinicAddress?: string | null;
|
||||||
|
vetAccountNumber?: string | null;
|
||||||
|
vetDoctorName?: string | null;
|
||||||
gender: BirdGender;
|
gender: BirdGender;
|
||||||
dateOfBirth: string | null;
|
dateOfBirth: string | null;
|
||||||
gotchaDay: string | null;
|
gotchaDay: string | null;
|
||||||
@@ -322,9 +335,9 @@ export const createBird = async ({
|
|||||||
publicProfileEnabled?: boolean;
|
publicProfileEnabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const result = await db.query<BirdRow>(
|
const result = await db.query<BirdRow>(
|
||||||
`INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled)
|
`INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled)
|
||||||
VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
|
VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)
|
||||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
|
||||||
[
|
[
|
||||||
birdId ?? null,
|
birdId ?? null,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -334,6 +347,10 @@ export const createBird = async ({
|
|||||||
motivators,
|
motivators,
|
||||||
demotivators,
|
demotivators,
|
||||||
favoriteSnack,
|
favoriteSnack,
|
||||||
|
vetClinicName,
|
||||||
|
vetClinicAddress,
|
||||||
|
vetAccountNumber,
|
||||||
|
vetDoctorName,
|
||||||
gender,
|
gender,
|
||||||
dateOfBirth,
|
dateOfBirth,
|
||||||
gotchaDay,
|
gotchaDay,
|
||||||
@@ -361,6 +378,10 @@ export const updateBird = async ({
|
|||||||
motivators,
|
motivators,
|
||||||
demotivators,
|
demotivators,
|
||||||
favoriteSnack,
|
favoriteSnack,
|
||||||
|
vetClinicName,
|
||||||
|
vetClinicAddress,
|
||||||
|
vetAccountNumber,
|
||||||
|
vetDoctorName,
|
||||||
gender,
|
gender,
|
||||||
dateOfBirth,
|
dateOfBirth,
|
||||||
gotchaDay,
|
gotchaDay,
|
||||||
@@ -382,6 +403,10 @@ export const updateBird = async ({
|
|||||||
motivators: string | null;
|
motivators: string | null;
|
||||||
demotivators: string | null;
|
demotivators: string | null;
|
||||||
favoriteSnack: string | null;
|
favoriteSnack: string | null;
|
||||||
|
vetClinicName: string | null;
|
||||||
|
vetClinicAddress: string | null;
|
||||||
|
vetAccountNumber: string | null;
|
||||||
|
vetDoctorName: string | null;
|
||||||
gender: BirdGender;
|
gender: BirdGender;
|
||||||
dateOfBirth: string | null;
|
dateOfBirth: string | null;
|
||||||
gotchaDay: string | null;
|
gotchaDay: string | null;
|
||||||
@@ -403,22 +428,26 @@ export const updateBird = async ({
|
|||||||
motivators = $5,
|
motivators = $5,
|
||||||
demotivators = $6,
|
demotivators = $6,
|
||||||
favorite_snack = $7,
|
favorite_snack = $7,
|
||||||
gender = $8,
|
vet_clinic_name = $8,
|
||||||
date_of_birth = $9,
|
vet_clinic_address = $9,
|
||||||
gotcha_day = $10,
|
vet_account_number = $10,
|
||||||
chart_color = $11,
|
vet_doctor_name = $11,
|
||||||
photo_data_url = $12,
|
gender = $12,
|
||||||
photo_object_key = $13,
|
date_of_birth = $13,
|
||||||
photo_content_type = $14,
|
gotcha_day = $14,
|
||||||
photo_updated_at = $15,
|
chart_color = $15,
|
||||||
notify_on_dob = $16,
|
photo_data_url = $16,
|
||||||
notify_on_gotcha_day = $17,
|
photo_object_key = $17,
|
||||||
public_profile_code = $18,
|
photo_content_type = $18,
|
||||||
public_profile_enabled = $19
|
photo_updated_at = $19,
|
||||||
|
notify_on_dob = $20,
|
||||||
|
notify_on_gotcha_day = $21,
|
||||||
|
public_profile_code = $22,
|
||||||
|
public_profile_enabled = $23
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND workspace_id = $20
|
AND workspace_id = $24
|
||||||
AND memorialized_at IS NULL
|
AND memorialized_at IS NULL
|
||||||
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||||
(
|
(
|
||||||
SELECT weight_grams::text
|
SELECT weight_grams::text
|
||||||
FROM weight_records
|
FROM weight_records
|
||||||
@@ -441,6 +470,10 @@ export const updateBird = async ({
|
|||||||
motivators,
|
motivators,
|
||||||
demotivators,
|
demotivators,
|
||||||
favoriteSnack,
|
favoriteSnack,
|
||||||
|
vetClinicName,
|
||||||
|
vetClinicAddress,
|
||||||
|
vetAccountNumber,
|
||||||
|
vetDoctorName,
|
||||||
gender,
|
gender,
|
||||||
dateOfBirth,
|
dateOfBirth,
|
||||||
gotchaDay,
|
gotchaDay,
|
||||||
@@ -482,7 +515,7 @@ export const memorializeBird = async ({
|
|||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND workspace_id = $2
|
AND workspace_id = $2
|
||||||
AND memorialized_at IS NULL
|
AND memorialized_at IS NULL
|
||||||
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||||
(
|
(
|
||||||
SELECT weight_grams::text
|
SELECT weight_grams::text
|
||||||
FROM weight_records
|
FROM weight_records
|
||||||
@@ -518,7 +551,7 @@ export const updateMemorialReminderPreference = async ({
|
|||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND workspace_id = $2
|
AND workspace_id = $2
|
||||||
AND memorialized_at IS NOT NULL
|
AND memorialized_at IS NOT NULL
|
||||||
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||||
(
|
(
|
||||||
SELECT weight_grams::text
|
SELECT weight_grams::text
|
||||||
FROM weight_records
|
FROM weight_records
|
||||||
@@ -558,7 +591,7 @@ export const transferBirdToWorkspace = async (birdId: string, sourceWorkspaceId:
|
|||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND workspace_id = $2
|
AND workspace_id = $2
|
||||||
AND memorialized_at IS NULL
|
AND memorialized_at IS NULL
|
||||||
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
|
||||||
(
|
(
|
||||||
SELECT weight_grams::text
|
SELECT weight_grams::text
|
||||||
FROM weight_records
|
FROM weight_records
|
||||||
@@ -659,7 +692,7 @@ export const completePendingBirdTransfersForOwner = async (ownerEmail: string, t
|
|||||||
failed += 1;
|
failed += 1;
|
||||||
const message =
|
const message =
|
||||||
typeof error === 'object' && error && 'code' in error && error.code === '23505'
|
typeof error === 'object' && error && 'code' in error && error.code === '23505'
|
||||||
? 'The receiving flock already has a bird using the same band/tag ID.'
|
? 'That band/tag ID is already in use in FlockPal.'
|
||||||
: error instanceof Error
|
: error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: 'Unable to complete pending bird transfer.';
|
: 'Unable to complete pending bird transfer.';
|
||||||
@@ -670,6 +703,93 @@ export const completePendingBirdTransfersForOwner = async (ownerEmail: string, t
|
|||||||
return { completed, failed };
|
return { completed, failed };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createBirdTransferCode = async ({
|
||||||
|
code,
|
||||||
|
birdId,
|
||||||
|
sourceWorkspaceId,
|
||||||
|
requestedByUserId,
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
birdId: string;
|
||||||
|
sourceWorkspaceId: number;
|
||||||
|
requestedByUserId: string;
|
||||||
|
}) => {
|
||||||
|
await db.query(
|
||||||
|
`UPDATE bird_transfer_codes
|
||||||
|
SET revoked_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE bird_id = $1
|
||||||
|
AND source_workspace_id = $2
|
||||||
|
AND completed_at IS NULL
|
||||||
|
AND revoked_at IS NULL`,
|
||||||
|
[birdId, sourceWorkspaceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await db.query<BirdTransferCodeRow>(
|
||||||
|
`INSERT INTO bird_transfer_codes (code, bird_id, source_workspace_id, requested_by_user_id)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, code, bird_id, source_workspace_id, requested_by_user_id, completed_at::text, completed_workspace_id, revoked_at::text, created_at`,
|
||||||
|
[code, birdId, sourceWorkspaceId, requestedByUserId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows[0] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOpenBirdTransferCode = async (code: string) => {
|
||||||
|
const result = await db.query<
|
||||||
|
BirdRow & {
|
||||||
|
transfer_code_id: string;
|
||||||
|
code: string;
|
||||||
|
source_workspace_id: number;
|
||||||
|
requested_by_user_id: string;
|
||||||
|
completed_at: string | null;
|
||||||
|
completed_workspace_id: number | null;
|
||||||
|
revoked_at: string | null;
|
||||||
|
transfer_code_created_at: string;
|
||||||
|
workspace_name: string;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
`SELECT
|
||||||
|
bird_transfer_codes.id AS transfer_code_id,
|
||||||
|
bird_transfer_codes.code,
|
||||||
|
bird_transfer_codes.source_workspace_id,
|
||||||
|
bird_transfer_codes.requested_by_user_id,
|
||||||
|
bird_transfer_codes.completed_at::text,
|
||||||
|
bird_transfer_codes.completed_workspace_id,
|
||||||
|
bird_transfer_codes.revoked_at::text,
|
||||||
|
bird_transfer_codes.created_at AS transfer_code_created_at,
|
||||||
|
workspaces.name AS workspace_name,
|
||||||
|
${birdSelectFields}
|
||||||
|
FROM bird_transfer_codes
|
||||||
|
INNER JOIN birds ON birds.id = bird_transfer_codes.bird_id
|
||||||
|
INNER JOIN workspaces ON workspaces.id = bird_transfer_codes.source_workspace_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT weight_grams, recorded_on
|
||||||
|
FROM weight_records
|
||||||
|
WHERE weight_records.bird_id = birds.id
|
||||||
|
ORDER BY recorded_on DESC
|
||||||
|
LIMIT 1
|
||||||
|
) latest ON TRUE
|
||||||
|
WHERE bird_transfer_codes.code = $1
|
||||||
|
AND bird_transfer_codes.completed_at IS NULL
|
||||||
|
AND bird_transfer_codes.revoked_at IS NULL
|
||||||
|
AND birds.workspace_id = bird_transfer_codes.source_workspace_id
|
||||||
|
AND birds.memorialized_at IS NULL`,
|
||||||
|
[code],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows[0] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markBirdTransferCodeCompleted = async (codeId: string, completedWorkspaceId: number) => {
|
||||||
|
await db.query(
|
||||||
|
`UPDATE bird_transfer_codes
|
||||||
|
SET completed_at = CURRENT_TIMESTAMP,
|
||||||
|
completed_workspace_id = $2
|
||||||
|
WHERE id = $1`,
|
||||||
|
[codeId, completedWorkspaceId],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const listWeightsForBird = async (birdId: string, workspaceId: number, days: number) => {
|
export const listWeightsForBird = async (birdId: string, workspaceId: number, days: number) => {
|
||||||
const result = await db.query<WeightRow>(
|
const result = await db.query<WeightRow>(
|
||||||
`SELECT id, bird_id, weight_grams, recorded_on::text, notes
|
`SELECT id, bird_id, weight_grams, recorded_on::text, notes
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ export type BirdRow = {
|
|||||||
motivators: string | null;
|
motivators: string | null;
|
||||||
demotivators: string | null;
|
demotivators: string | null;
|
||||||
favorite_snack: string | null;
|
favorite_snack: string | null;
|
||||||
|
vet_clinic_name: string | null;
|
||||||
|
vet_clinic_address: string | null;
|
||||||
|
vet_account_number: string | null;
|
||||||
|
vet_doctor_name: string | null;
|
||||||
gender: BirdGender;
|
gender: BirdGender;
|
||||||
date_of_birth: string | null;
|
date_of_birth: string | null;
|
||||||
gotcha_day: string | null;
|
gotcha_day: string | null;
|
||||||
@@ -158,6 +162,18 @@ export type PendingBirdTransferRow = {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BirdTransferCodeRow = {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
bird_id: string;
|
||||||
|
source_workspace_id: number;
|
||||||
|
requested_by_user_id: string;
|
||||||
|
completed_at: string | null;
|
||||||
|
completed_workspace_id: number | null;
|
||||||
|
revoked_at: string | null;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type WeightRow = {
|
export type WeightRow = {
|
||||||
id: string;
|
id: string;
|
||||||
bird_id: string;
|
bird_id: string;
|
||||||
|
|||||||
+43
-1
@@ -208,6 +208,10 @@ Role requirements are called out per endpoint below. If the signed-in member lac
|
|||||||
"name": "Kiwi",
|
"name": "Kiwi",
|
||||||
"tagId": "FP-001",
|
"tagId": "FP-001",
|
||||||
"species": "Cockatiel",
|
"species": "Cockatiel",
|
||||||
|
"vetClinicName": "Avian Care Center",
|
||||||
|
"vetClinicAddress": "123 Feather Lane, Raleigh, NC",
|
||||||
|
"vetAccountNumber": "FP-1001",
|
||||||
|
"vetDoctorName": "Dr. Rivera",
|
||||||
"gender": "female",
|
"gender": "female",
|
||||||
"dateOfBirth": "2023-05-10",
|
"dateOfBirth": "2023-05-10",
|
||||||
"gotchaDay": "2023-08-21",
|
"gotchaDay": "2023-08-21",
|
||||||
@@ -793,6 +797,10 @@ Request body:
|
|||||||
"name": "Kiwi",
|
"name": "Kiwi",
|
||||||
"tagId": "FP-001",
|
"tagId": "FP-001",
|
||||||
"species": "Cockatiel",
|
"species": "Cockatiel",
|
||||||
|
"vetClinicName": "Avian Care Center",
|
||||||
|
"vetClinicAddress": "123 Feather Lane, Raleigh, NC",
|
||||||
|
"vetAccountNumber": "FP-1001",
|
||||||
|
"vetDoctorName": "Dr. Rivera",
|
||||||
"gender": "female",
|
"gender": "female",
|
||||||
"dateOfBirth": "2023-05-10",
|
"dateOfBirth": "2023-05-10",
|
||||||
"gotchaDay": "2023-08-21",
|
"gotchaDay": "2023-08-21",
|
||||||
@@ -805,7 +813,7 @@ Request body:
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- `dateOfBirth`, `gotchaDay`, and `photoDataUrl` may be omitted or sent as empty strings
|
- `dateOfBirth`, `gotchaDay`, `photoDataUrl`, and veterinary info fields may be omitted or sent as empty strings
|
||||||
- `chartColor` defaults to `#cb3a35`
|
- `chartColor` defaults to `#cb3a35`
|
||||||
|
|
||||||
Response `201`:
|
Response `201`:
|
||||||
@@ -889,6 +897,40 @@ Possible errors:
|
|||||||
- `409` if that owner email owns more than one receiving flock
|
- `409` if that owner email owns more than one receiving flock
|
||||||
- `409` if the destination flock already has a bird using the same `tagId`
|
- `409` if the destination flock already has a bird using the same `tagId`
|
||||||
|
|
||||||
|
#### `POST /api/birds/:birdId/transfer-code`
|
||||||
|
|
||||||
|
Requires a browser session, write access, and role `owner` or `assistant`. Creates a unique transfer code for a bird. Creating a new open code for the same bird revokes earlier unused codes for that bird.
|
||||||
|
|
||||||
|
Response `201`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"transferCode": {
|
||||||
|
"code": "secure-code",
|
||||||
|
"bird": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `POST /api/bird-transfer-codes/:code/accept`
|
||||||
|
|
||||||
|
Requires a browser session, write access, and role `owner` or `assistant`. Accepts a transfer code into the signed-in user's active flock.
|
||||||
|
|
||||||
|
Response `200`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bird": {},
|
||||||
|
"sourceWorkspaceName": "Previous Flock",
|
||||||
|
"workspace": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Possible errors:
|
||||||
|
|
||||||
|
- `404` if the code does not exist, was revoked, was already used, or the bird is no longer available
|
||||||
|
- `409` if the bird is already in the active flock or the active flock already has the same `tagId`
|
||||||
|
|
||||||
#### `DELETE /api/birds/:birdId`
|
#### `DELETE /api/birds/:birdId`
|
||||||
|
|
||||||
Requires auth with write access and role `owner`, `assistant`, or `caregiver`. Deletes a bird.
|
Requires auth with write access and role `owner`, `assistant`, or `caregiver`. Deletes a bird.
|
||||||
|
|||||||
+840
-52
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
@@ -1211,6 +1211,7 @@ textarea {
|
|||||||
|
|
||||||
.bird-detail-tab .info-tab-icon,
|
.bird-detail-tab .info-tab-icon,
|
||||||
.bird-detail-tab .note-tab-icon,
|
.bird-detail-tab .note-tab-icon,
|
||||||
|
.bird-detail-tab .report-tab-icon,
|
||||||
.bird-detail-tab .audit-tab-icon,
|
.bird-detail-tab .audit-tab-icon,
|
||||||
.bird-detail-tab .vet-tab-icon {
|
.bird-detail-tab .vet-tab-icon {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@@ -1706,6 +1707,44 @@ label {
|
|||||||
accent-color: var(--accent-green);
|
accent-color: var(--accent-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: min(100%, 420px);
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
border: 1px solid rgba(53, 129, 98, 0.24);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.62);
|
||||||
|
box-shadow: 0 12px 24px rgba(86, 63, 34, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0.1rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
accent-color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row span {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.15rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row strong {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 0.98rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
.primary-button {
|
.primary-button {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
|
|||||||
Reference in New Issue
Block a user