diff --git a/bun.lock b/bun.lock index 563e8f3..96a5ff6 100644 --- a/bun.lock +++ b/bun.lock @@ -558,7 +558,7 @@ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - "framer-motion": ["framer-motion@12.28.2", "", { "dependencies": { "motion-dom": "^12.28.2", "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-LNzKJo1VWz3RqNSeBzMbDIdLLpUw7hp/PpsvOpDlLqT1KlAAGsoSZ3OxNAKmS89AUeRv0rBbYgFLAWzO33xzDw=="], + "framer-motion": ["framer-motion@12.29.0", "", { "dependencies": { "motion-dom": "^12.29.0", "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -746,7 +746,7 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "motion-dom": ["motion-dom@12.28.2", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-Lu+N4CSyc0JZlssX1OvdAgHanP8SuXCa4ZU/dEjGbtwZOjC2BH5e7MfDDi/Kgnzsu+Oyw4QYKhgFici4LlLiyg=="], + "motion-dom": ["motion-dom@12.29.0", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="], "motion-utils": ["motion-utils@12.27.2", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="], diff --git a/package-lock.json b/package-lock.json index bcfd352..8c863a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.5.0", + "jspdf": "^4.0.0", "lucide-react": "^0.482.0", "next": "15.3.8", "next-themes": "^0.4.6", @@ -53,6 +54,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", @@ -3369,6 +3379,19 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.0.11", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.11.tgz", @@ -3389,6 +3412,13 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.26.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", @@ -4185,6 +4215,16 @@ "node": ">=0.12.0" } }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4352,6 +4392,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4435,6 +4495,18 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4450,6 +4522,16 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -4617,6 +4699,16 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5321,6 +5413,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -5340,6 +5443,12 @@ "is-retry-allowed": "^3.0.0" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5759,6 +5868,20 @@ "node": ">= 0.4" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5837,6 +5960,12 @@ "node": ">= 0.4" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6333,6 +6462,23 @@ "json5": "lib/cli.js" } }, + "node_modules/jspdf": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz", + "integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -7083,6 +7229,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7123,6 +7275,13 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7240,6 +7399,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7369,6 +7538,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -7456,6 +7632,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rspack-resolver": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/rspack-resolver/-/rspack-resolver-1.1.2.tgz", @@ -7838,6 +8024,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -8031,6 +8227,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz", @@ -8066,6 +8272,16 @@ "node": ">=6" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", @@ -8358,6 +8574,16 @@ } } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 2b32795..b029286 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.5.0", + "jspdf": "^4.0.0", "lucide-react": "^0.482.0", "next": "15.3.8", "next-themes": "^0.4.6", diff --git a/src/components/escrow/EscrowDetails.tsx b/src/components/escrow/EscrowDetails.tsx index fb583bb..8c4986a 100644 --- a/src/components/escrow/EscrowDetails.tsx +++ b/src/components/escrow/EscrowDetails.tsx @@ -24,6 +24,7 @@ import { } from "@/utils/transactionFetcher"; import { LedgerBalancePanel } from "@/components/escrow/LedgerBalancePanel"; import { useIsMobile } from "@/hooks/useIsMobile"; +import { ExportPdfButton } from "./ExportPdfButton"; // ⬇️ New hooks import { useEscrowData } from "@/hooks/useEscrowData"; diff --git a/src/components/escrow/ExportPdfButton.tsx b/src/components/escrow/ExportPdfButton.tsx new file mode 100644 index 0000000..f2342b3 --- /dev/null +++ b/src/components/escrow/ExportPdfButton.tsx @@ -0,0 +1,54 @@ +import { Button } from "@/components/ui/button"; +import { FileDown } from "lucide-react"; +import { generateEscrowPdf } from "@/utils/pdf-export"; +import type { OrganizedEscrowData } from "@/mappers/escrow-mapper"; +import type { NetworkType } from "@/lib/network-config"; + +interface ExportPdfButtonProps { + organized: OrganizedEscrowData; + network: NetworkType; + contractId?: string; + initialEscrowId?: string; +} + +export function ExportPdfButton({ + organized, + network, + contractId, + initialEscrowId, +}: ExportPdfButtonProps) { + const handleExport = async () => { + const contractIdToUse = + organized.properties.escrow_id || + contractId || + initialEscrowId; + + if (!contractIdToUse) { + alert("Error: Contract ID is required to export PDF."); + return; + } + + try { + await generateEscrowPdf({ + organized, + network, + contractId: contractIdToUse, + }); + } catch (error) { + console.error("Error generating PDF:", error); + alert("Error generating PDF. Please try again."); + } + }; + + return ( + + ); +} diff --git a/src/utils/pdf-export.ts b/src/utils/pdf-export.ts new file mode 100644 index 0000000..f306d8b --- /dev/null +++ b/src/utils/pdf-export.ts @@ -0,0 +1,282 @@ +import jsPDF from 'jspdf'; +import type { OrganizedEscrowData } from '@/mappers/escrow-mapper'; +import type { NetworkType } from '@/lib/network-config'; +import { ROLE_MAPPING } from '@/lib/escrow-constants'; + +interface PdfExportOptions { + organized: OrganizedEscrowData; + network: NetworkType; + contractId: string; +} + +async function imageToBase64(url: string): Promise { + try { + const response = await fetch(url); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } catch (error) { + console.warn('Failed to load logo image:', error); + return ''; + } +} + +export async function generateEscrowPdf({ organized, network, contractId }: PdfExportOptions): Promise { + const doc = new jsPDF({ + orientation: 'portrait', + unit: 'mm', + format: 'a4', + }); + + const pageWidth = doc.internal.pageSize.getWidth(); + const pageHeight = doc.internal.pageSize.getHeight(); + const margin = 20; + const contentWidth = pageWidth - 2 * margin; + let yPosition = margin; + + const checkPageBreak = (requiredSpace: number) => { + if (yPosition + requiredSpace > pageHeight - margin) { + doc.addPage(); + yPosition = margin; + return true; + } + return false; + }; + + const addText = (text: string, fontSize: number, isBold = false, color: [number, number, number] = [0, 0, 0]) => { + doc.setFontSize(fontSize); + doc.setFont('helvetica', isBold ? 'bold' : 'normal'); + doc.setTextColor(color[0], color[1], color[2]); + + const lines = doc.splitTextToSize(text, contentWidth); + lines.forEach((line: string) => { + checkPageBreak(fontSize * 0.5); + doc.text(line, margin, yPosition); + yPosition += fontSize * 0.5; + }); + yPosition += 3; + }; + + const addSectionHeader = (title: string) => { + checkPageBreak(15); + yPosition += 5; + addText(title, 14, true, [0, 51, 102]); + doc.setDrawColor(0, 51, 102); + doc.setLineWidth(0.5); + doc.line(margin, yPosition - 2, pageWidth - margin, yPosition - 2); + yPosition += 3; + }; + + const addKeyValue = (key: string, value: string, indent = 0) => { + checkPageBreak(8); + const xPos = margin + indent; + doc.setFontSize(10); + doc.setFont('helvetica', 'bold'); + doc.setTextColor(60, 60, 60); + doc.text(`${key}:`, xPos, yPosition); + + const valueX = xPos + 50; + doc.setFont('helvetica', 'normal'); + doc.setTextColor(0, 0, 0); + const valueLines = doc.splitTextToSize(value, contentWidth - 50 - indent); + valueLines.forEach((line: string) => { + doc.text(line, valueX, yPosition); + yPosition += 5; + }); + yPosition += 2; + }; + + const headerHeight = 40; + doc.setFillColor(0, 51, 102); + doc.rect(0, 0, pageWidth, headerHeight, 'F'); + + try { + const logoBase64 = await imageToBase64('/logo.png'); + if (logoBase64) { + doc.addImage(logoBase64, 'PNG', 15, 11, 18, 18); + } + } catch { + // Logo failed to load, continue without it + } + + const logoWidth = 18; + const logoSpacing = 5; + const textStartX = 15 + logoWidth + logoSpacing; + + doc.setTextColor(255, 255, 255); + doc.setFontSize(20); + doc.setFont('helvetica', 'bold'); + doc.text('Trustless Work', textStartX, 18); + + doc.setFontSize(16); + doc.setFont('helvetica', 'normal'); + doc.text('Escrow Report', textStartX, 26); + + const rightMargin = 15; + const labelWidth = 25; + const valueWidth = 45; + const rightLabelX = pageWidth - rightMargin - labelWidth - valueWidth; + const rightValueX = pageWidth - rightMargin - valueWidth; + + doc.setFontSize(9); + doc.setFont('helvetica', 'bold'); + doc.text('Network:', rightLabelX, 16); + doc.setFont('helvetica', 'normal'); + const networkText = network === 'mainnet' ? 'Mainnet' : 'Testnet'; + doc.text(networkText, rightValueX, 16); + + const exportDate = new Date().toLocaleString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + doc.setFont('helvetica', 'bold'); + doc.text('Exported:', rightLabelX, 24); + doc.setFont('helvetica', 'normal'); + const dateLines = doc.splitTextToSize(exportDate, valueWidth); + let dateY = 24; + dateLines.forEach((line: string) => { + doc.text(line, rightValueX, dateY); + dateY += 4; + }); + + yPosition = 50; + + addSectionHeader('Escrow Summary'); + + addKeyValue('Escrow ID', contractId); + addKeyValue('Title', organized.title || 'N/A'); + addKeyValue('Description', organized.description || 'N/A'); + addKeyValue('Escrow Type', organized.escrowType === 'multi-release' ? 'Multi-Release' : 'Single-Release'); + + if (organized.properties.trustline) { + addKeyValue('Asset / Trustline', organized.properties.trustline); + } + + if (organized.properties.amount) { + addKeyValue('Total Amount', `${organized.properties.amount} ${organized.properties.trustline ? organized.properties.trustline.split(':')[0] : ''}`); + } + + if (organized.properties.balance) { + addKeyValue('Current Balance', `${organized.properties.balance} ${organized.properties.trustline ? organized.properties.trustline.split(':')[0] : ''}`); + } + + if (organized.properties.platform_fee) { + addKeyValue('Platform Fee', organized.properties.platform_fee); + } + + if (organized.properties.amount) { + const amount = parseFloat(organized.properties.amount); + if (!isNaN(amount)) { + const trustlessWorkFee = (amount * 0.003).toFixed(2); + addKeyValue('Trustless Work Fee (0.3%)', trustlessWorkFee); + } + } + + if (organized.properties.engagement_id) { + addKeyValue('Engagement ID', organized.properties.engagement_id); + } + + addSectionHeader('Escrow Status'); + + const getStatusText = () => { + if (organized.flags.resolved_flag === 'true') return 'Resolved'; + if (organized.flags.dispute_flag === 'true') return 'Disputed'; + if (organized.flags.release_flag === 'true') return 'Released'; + return 'Active'; + }; + + addKeyValue('Current Status', getStatusText()); + addKeyValue('Dispute Flag', organized.flags.dispute_flag === 'true' ? 'Yes' : 'No'); + addKeyValue('Release Flag', organized.flags.release_flag === 'true' ? 'Yes' : 'No'); + addKeyValue('Resolved Flag', organized.flags.resolved_flag === 'true' ? 'Yes' : 'No'); + addKeyValue('Progress', `${organized.progress.toFixed(1)}%`); + + addSectionHeader('Assigned Roles'); + + if (Object.keys(organized.roles).length === 0) { + addText('No roles assigned', 10, false, [100, 100, 100]); + } else { + Object.entries(organized.roles).forEach(([roleKey, address]) => { + const roleName = ROLE_MAPPING[roleKey] || roleKey; + addKeyValue(roleName, address, 5); + }); + } + + addSectionHeader('Milestones'); + + if (organized.milestones.length === 0) { + addText('No milestones defined', 10, false, [100, 100, 100]); + } else { + organized.milestones.forEach((milestone, index) => { + checkPageBreak(30); + yPosition += 3; + + doc.setFontSize(11); + doc.setFont('helvetica', 'bold'); + doc.setTextColor(0, 51, 102); + doc.text(`Milestone ${milestone.id + 1}: ${milestone.title}`, margin + 5, yPosition); + yPosition += 6; + + addKeyValue('Description', milestone.description || 'N/A', 10); + + if (milestone.amount) { + addKeyValue('Amount', milestone.amount, 10); + } + + const statusText = milestone.approved + ? 'Approved' + : milestone.release_flag + ? 'Released' + : milestone.dispute_flag + ? 'Disputed' + : milestone.resolved_flag + ? 'Resolved' + : 'Pending'; + addKeyValue('Status', statusText, 10); + + if (milestone.signer) { + addKeyValue('Signer', milestone.signer, 10); + } + + if (milestone.approver) { + addKeyValue('Approver', milestone.approver, 10); + } + + yPosition += 3; + + if (index < organized.milestones.length - 1) { + doc.setDrawColor(200, 200, 200); + doc.setLineWidth(0.2); + doc.line(margin + 5, yPosition, pageWidth - margin - 5, yPosition); + yPosition += 3; + } + }); + } + + const totalPages = doc.getNumberOfPages(); + for (let i = 1; i <= totalPages; i++) { + doc.setPage(i); + + doc.setDrawColor(200, 200, 200); + doc.setLineWidth(0.2); + doc.line(margin, pageHeight - 15, pageWidth - margin, pageHeight - 15); + + doc.setFontSize(8); + doc.setFont('helvetica', 'normal'); + doc.setTextColor(100, 100, 100); + doc.text('Generated by Trustless Work Escrow Viewer', margin, pageHeight - 10); + doc.text('https://trustless.work', pageWidth - margin - 40, pageHeight - 10); + doc.text(`Page ${i} of ${totalPages}`, pageWidth - margin - 20, pageHeight - 5, { align: 'right' }); + } + + const dateStr = new Date().toISOString().split('T')[0]; + const filename = `escrow-report-${contractId.substring(0, 8)}-${dateStr}.pdf`; + doc.save(filename); +}