Merge branch 'dev/randy' into 'feat/FE/US-338/expense-report'

[FIX/FE][US#338] Fixing Export PDF Using jspdf Library

See merge request mbugroup/lti-web-client!112
This commit is contained in:
Rivaldi A N S
2025-12-20 02:17:37 +00:00
10 changed files with 448 additions and 69985 deletions
-2
View File
@@ -165,8 +165,6 @@ deploy:staging:
environment: environment:
name: staging name: staging
url: https://stg-lti-erp.mbugroup.id url: https://stg-lti-erp.mbugroup.id
# ====== PRODUCTION ====== # ====== PRODUCTION ======
# build:production: # build:production:
# <<: *build_template # <<: *build_template
+227
View File
@@ -14,6 +14,8 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formik": "^2.4.6", "formik": "^2.4.6",
"jspdf": "^3.0.4",
"jspdf-autotable": "^5.0.2",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.9", "next": "15.5.9",
"react": "19.1.0", "react": "19.1.0",
@@ -1845,12 +1847,25 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.2", "version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
@@ -1879,6 +1894,13 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.2", "version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
@@ -2776,6 +2798,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -2925,6 +2957,26 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3020,6 +3072,18 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js": {
"version": "3.47.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
"integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -3057,6 +3121,16 @@
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT" "license": "MIT"
}, },
"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": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -3276,6 +3350,16 @@
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"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": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3995,6 +4079,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-png/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/fastq": { "node_modules/fastq": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -4005,6 +4106,12 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"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": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4492,6 +4599,20 @@
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
"license": "ISC" "license": "ISC"
}, },
"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/husky": { "node_modules/husky": {
"version": "9.1.7", "version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
@@ -4571,6 +4692,12 @@
"node": ">= 0.4" "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": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -5106,6 +5233,32 @@
"json5": "lib/cli.js" "json5": "lib/cli.js"
} }
}, },
"node_modules/jspdf": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf-autotable": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3"
}
},
"node_modules/jsx-ast-utils": { "node_modules/jsx-ast-utils": {
"version": "3.3.5", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -6010,6 +6163,13 @@
"node": ">=8" "node": ">=8"
} }
}, },
"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": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -6163,6 +6323,16 @@
], ],
"license": "MIT" "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/react": { "node_modules/react": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
@@ -6321,6 +6491,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -6413,6 +6590,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -6762,6 +6949,16 @@
"dev": true, "dev": true,
"license": "MIT" "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/stop-iteration-iterator": { "node_modules/stop-iteration-iterator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -6981,6 +7178,16 @@
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==",
"license": "ISC" "license": "ISC"
}, },
"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/swr": { "node_modules/swr": {
"version": "2.3.6", "version": "2.3.6",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
@@ -7025,6 +7232,16 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"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/tiny-case": { "node_modules/tiny-case": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
@@ -7397,6 +7614,16 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vite-compatible-readable-stream": { "node_modules/vite-compatible-readable-stream": {
"version": "3.6.1", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
+2
View File
@@ -17,6 +17,8 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formik": "^2.4.6", "formik": "^2.4.6",
"jspdf": "^3.0.4",
"jspdf-autotable": "^5.0.2",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.9", "next": "15.5.9",
"react": "19.1.0", "react": "19.1.0",
@@ -74,17 +74,11 @@ const ClosingFinanceTable = ({
const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance) const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance)
? [ ? [
// Penjualan group // Pembelian group
{ ...finance.data.profit_loss.data.pembelian.map((item) => ({
label: 'Penjualan', label: 'Pembelian',
group_name: 'Penjualan', group_name: 'Pembelian',
group_index: 0, group_index: 1,
isGroupHeader: true as const,
},
...finance.data.profit_loss.data.penjualan.map((item) => ({
label: 'Penjualan',
group_name: 'Penjualan',
group_index: 0,
type: item.type, type: item.type,
rp_per_bird: item.rp_per_bird, rp_per_bird: item.rp_per_bird,
rp_per_kg: item.rp_per_kg, rp_per_kg: item.rp_per_kg,
@@ -103,17 +97,11 @@ const ClosingFinanceTable = ({
finance.data.profit_loss.data.summary.gross_profit.rp_per_kg, finance.data.profit_loss.data.summary.gross_profit.rp_per_kg,
amount: finance.data.profit_loss.data.summary.gross_profit.amount, amount: finance.data.profit_loss.data.summary.gross_profit.amount,
}, },
// Pembelian group // Penjualan group
{ ...finance.data.profit_loss.data.penjualan.map((item) => ({
label: 'Pembelian', label: 'Penjualan',
group_name: 'Pembelian', group_name: 'Penjualan',
group_index: 1, group_index: 0,
isGroupHeader: true as const,
},
...finance.data.profit_loss.data.pembelian.map((item) => ({
label: 'Pembelian',
group_name: 'Pembelian',
group_index: 1,
type: item.type, type: item.type,
rp_per_bird: item.rp_per_bird, rp_per_bird: item.rp_per_bird,
rp_per_kg: item.rp_per_kg, rp_per_kg: item.rp_per_kg,
@@ -369,16 +357,16 @@ const ClosingFinanceTable = ({
data={profitLossTableData} data={profitLossTableData}
columns={[ columns={[
{ {
header: 'Type', header: 'Jenis',
enableSorting: false, enableSorting: false,
accessorFn: (item) => item.type, accessorFn: (item) => item.type,
cell: (item) => ( cell: (item) => (
<div className='ps-6'> <div className=''>
{formatTitleCase(item.row.original.type || '-')} {formatTitleCase(item.row.original.type || '-')}
</div> </div>
), ),
footer: (item) => ( footer: (item) => (
<div className='font-bold'> <div className='font-bold uppercase'>
{isResponseSuccess(finance) {isResponseSuccess(finance)
? formatTitleCase( ? formatTitleCase(
finance.data.profit_loss.data.summary.net_profit finance.data.profit_loss.data.summary.net_profit
@@ -446,7 +434,7 @@ const ClosingFinanceTable = ({
<td <td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName} className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
> >
<div className='font-bold'> <div className='font-bold ps-6 uppercase'>
{formatTitleCase(rowData.label ?? '-')} {formatTitleCase(rowData.label ?? '-')}
</div> </div>
</td> </td>
@@ -68,7 +68,7 @@ const ProjectFlockDetail = ({
latestApproval: projectFlock?.approval, latestApproval: projectFlock?.approval,
approvalLines: PROJECT_FLOCK_APPROVAL_LINE, approvalLines: PROJECT_FLOCK_APPROVAL_LINE,
moduleName: 'PROJECT_FLOCKS', moduleName: 'PROJECT_FLOCKS',
moduleId: projectFlock?.id.toString() ?? '', moduleId: projectFlock?.id?.toString() ?? '',
}); });
const { approvals: kandangApprovals, isLoading: kandangApprovalsLoading } = const { approvals: kandangApprovals, isLoading: kandangApprovalsLoading } =
@@ -47,9 +47,7 @@ import Card from '@/components/Card';
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable'; import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
import { Nonstock } from '@/types/api/master-data/nonstock'; import { Nonstock } from '@/types/api/master-data/nonstock';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import Link from 'next/link';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { formatDate } from '@/lib/helper';
interface ProjectFlockFormProps { interface ProjectFlockFormProps {
formType?: 'add' | 'edit' | 'detail'; formType?: 'add' | 'edit' | 'detail';
@@ -260,7 +258,9 @@ const ProjectFlockForm = ({
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('category', (val as OptionType)?.value); formik.setFieldValue('category', (val as OptionType)?.value);
formik.setFieldValue('category_option', val); formik.setFieldValue('category_option', val);
formik.setFieldTouched('category', true); if (val == null) {
formik.setFieldTouched('category', true);
}
}; };
// Submit Handler // Submit Handler
@@ -788,7 +788,7 @@ const ProjectFlockForm = ({
} }
errorMessage={formik.errors.area_id as string} errorMessage={formik.errors.area_id as string}
isClearable isClearable
isDisabled={formType === 'detail'} isDisabled={formType != 'add'}
/> />
<SelectInput <SelectInput
required required
@@ -807,7 +807,7 @@ const ProjectFlockForm = ({
} }
errorMessage={formik.errors.location_id as string} errorMessage={formik.errors.location_id as string}
isClearable isClearable
isDisabled={formType === 'detail' || disabledLocation} isDisabled={formType != 'add' || disabledLocation}
/> />
<SelectInput <SelectInput
required required
@@ -837,7 +837,7 @@ const ProjectFlockForm = ({
} }
errorMessage={formik.errors.flock_name as string} errorMessage={formik.errors.flock_name as string}
isClearable isClearable
isDisabled={formType === 'detail'} isDisabled={formType != 'add'}
/> />
<SelectInput <SelectInput
required required
@@ -851,7 +851,7 @@ const ProjectFlockForm = ({
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)} isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
errorMessage={formik.errors.fcr_id as string} errorMessage={formik.errors.fcr_id as string}
isClearable isClearable
isDisabled={formType === 'detail'} isDisabled={formType != 'add'}
/> />
<SelectInput <SelectInput
required required
@@ -864,7 +864,7 @@ const ProjectFlockForm = ({
} }
errorMessage={formik.errors.category as string} errorMessage={formik.errors.category as string}
isClearable isClearable
isDisabled={formType === 'detail'} isDisabled={formType != 'add'}
/> />
<NumberInput <NumberInput
name='period' name='period'
@@ -1,602 +1,218 @@
import { ReportExpense } from '@/types/api/report/report-expense'; import { ReportExpense } from '@/types/api/report/report-expense';
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
import { formatCurrency, formatDate } from '@/lib/helper'; import { formatCurrency, formatDate } from '@/lib/helper';
import pdfStyles from '@/components/pages/report/expense/pdf/styles/ReportExpenseStyles'; import jsPDF from 'jspdf';
import toast from 'react-hot-toast'; import autoTable, { UserOptions } from 'jspdf-autotable';
interface jsPDFWithAutoTable extends jsPDF {
lastAutoTable: {
finalY: number;
};
}
export interface PDFParams { export interface PDFParams {
location_name?: string; location_name?: string;
supplier_name?: string; supplier_name?: string;
kandang_name?: string;
nonstock_name?: string;
category?: string;
realization_date?: string; realization_date?: string;
search?: string;
} }
const getStatusStyle = (action?: string) => { const getStatusColor = (action?: string): [number, number, number] => {
switch (action) { switch (action) {
case 'APPROVED': case 'APPROVED':
return { backgroundColor: '#dcfce7' }; case 'Selesai': // Berdasarkan data sumber
return [220, 252, 231]; // Hijau muda (#dcfce7)
case 'REJECTED': case 'REJECTED':
return { backgroundColor: '#fee2e2' }; return [254, 226, 226]; // Merah muda (#fee2e2)
case 'Realisasi': // Berdasarkan data sumber
return [254, 243, 199]; // Kuning/Amber muda (#fef3c7)
default: default:
return { backgroundColor: '#fef3c7' }; return [255, 255, 255]; // Putih
} }
}; };
const PDFDocument = ({
data,
params,
}: {
data: ReportExpense[];
params: PDFParams;
}) => {
// Group data by supplier
const groupedBySupplier = (() => {
const groups: Record<string, ReportExpense[]> = {};
data.forEach((item) => {
const supplierName = item.supplier?.name || 'Unknown Supplier';
if (!groups[supplierName]) {
groups[supplierName] = [];
}
groups[supplierName].push(item);
});
return groups;
})();
// Calculate grand totals
const grandTotals = data.reduce(
(acc, item) => {
const pengajuanTotal =
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0);
const realisasiTotal =
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0);
return {
pengajuan: acc.pengajuan + pengajuanTotal,
realisasi: acc.realisasi + realisasiTotal,
};
},
{ pengajuan: 0, realisasi: 0 }
);
return (
<Document>
<Page size='A4' orientation='landscape' style={pdfStyles.page}>
{/* Header Section */}
<View style={pdfStyles.header}>
<Image
src={'https://placehold.co/120x30/png'}
style={pdfStyles.logo}
id={'mbu-logo'}
/>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
{/* Report Title */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.title}>LAPORAN BIAYA OPERASIONAL</Text>
<View style={pdfStyles.poInfo}>
<Text>Tanggal Cetak: {formatDate(new Date(), 'DD MMM YYYY')}</Text>
<Text>Total Data: {data.length} transaksi</Text>
</View>
</View>
{/* Filters Info if any */}
{(params.location_name ||
params.supplier_name ||
params.realization_date) && (
<View style={{ marginBottom: 10, fontSize: 8 }}>
{params.location_name && (
<Text>Lokasi: {params.location_name}</Text>
)}
{params.supplier_name && (
<Text>Supplier: {params.supplier_name}</Text>
)}
{params.realization_date && (
<Text>
Tanggal Realisasi:{' '}
{formatDate(params.realization_date, 'DD MMM YYYY')}
</Text>
)}
</View>
)}
{/* Grouped Tables by Supplier */}
{Object.entries(groupedBySupplier).map(
([supplierName, items], groupIndex) => {
const supplierTotals = items.reduce(
(acc, item) => {
const pengajuanTotal =
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0);
const realisasiTotal =
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0);
return {
pengajuan: acc.pengajuan + pengajuanTotal,
realisasi: acc.realisasi + realisasiTotal,
};
},
{ pengajuan: 0, realisasi: 0 }
);
return (
<View key={groupIndex} style={pdfStyles.allocationSection}>
{/* Supplier Header */}
<Text style={pdfStyles.sectionTitle}>{supplierName}</Text>
{/* Table */}
<View style={pdfStyles.table}>
{/* Header Row 1: Group Headers */}
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View
style={[
pdfStyles.tableCellNarrowHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>No</Text>
</View>
<View
style={[
pdfStyles.tableCellWrapHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>No. PO</Text>
</View>
<View
style={[
pdfStyles.tableCellWrapHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>No. Referensi</Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Tgl Realisasi</Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Tgl Transaksi</Text>
</View>
<View
style={[
pdfStyles.tableCellXSmallHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Kategori</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Produk</Text>
</View>
<View
style={[
pdfStyles.tableCellSmallHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Lokasi</Text>
</View>
<View
style={[
pdfStyles.tableCellSmallHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Kandang</Text>
</View>
{/* Pengajuan Group */}
<View
style={[
pdfStyles.tableCellXSmallHeader,
{ borderBottomWidth: 0, borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{
borderBottomWidth: 0,
borderRightWidth: 0,
textAlign: 'center',
},
]}
>
<Text>Pengajuan</Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text></Text>
</View>
{/* Realisasi Group */}
<View
style={[
pdfStyles.tableCellXSmallHeader,
{ borderBottomWidth: 0, borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{
borderBottomWidth: 0,
borderRightWidth: 0,
textAlign: 'center',
},
]}
>
<Text>Realisasi</Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Status BOP</Text>
</View>
</View>
{/* Header Row 2: Sub Headers */}
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellNarrowHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellWrapHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellWrapHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellXSmallHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellSmallHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellSmallHeader}>
<Text></Text>
</View>
{/* Pengajuan sub-headers */}
<View style={pdfStyles.tableCellXSmallHeader}>
<Text>Qty</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text>Harga</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text>Total</Text>
</View>
{/* Realisasi sub-headers */}
<View style={pdfStyles.tableCellXSmallHeader}>
<Text>Qty</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text>Harga</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text>Total</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text></Text>
</View>
</View>
{/* Table Body */}
{items.map((item, index) => {
const pengajuanTotal =
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0);
const realisasiTotal =
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0);
return (
<View key={index} style={pdfStyles.tableRow}>
<View style={pdfStyles.tableCellNarrow}>
<Text>{index + 1}</Text>
</View>
<View style={pdfStyles.tableCellWrap}>
<Text>{item.po_number || '-'}</Text>
</View>
<View style={pdfStyles.tableCellWrap}>
<Text>{item.reference_number || '-'}</Text>
</View>
<View style={pdfStyles.tableCellMedium}>
<Text>
{formatDate(item.realization_date, 'DD MMM YY')}
</Text>
</View>
<View style={pdfStyles.tableCellMedium}>
<Text>
{formatDate(item.transaction_date, 'DD MMM YY')}
</Text>
</View>
<View style={pdfStyles.tableCellXSmall}>
<Text>
{item.category?.split('-').join(' ') || '-'}
</Text>
</View>
<View style={pdfStyles.tableCell}>
<Text>{item.pengajuan?.nonstock?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCellSmall}>
<Text>{item.kandang?.location?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCellSmall}>
<Text>{item.kandang?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCellRightXSmall}>
<Text>
{(item.pengajuan?.qty || 0).toLocaleString('id-ID')}
</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text>
{formatCurrency(item.pengajuan?.price || 0)}
</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text>{formatCurrency(pengajuanTotal)}</Text>
</View>
<View style={pdfStyles.tableCellRightXSmall}>
<Text>
{(item.realisasi?.qty || 0).toLocaleString('id-ID')}
</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text>
{formatCurrency(item.realisasi?.price || 0)}
</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text>{formatCurrency(realisasiTotal)}</Text>
</View>
<View style={pdfStyles.tableCellMedium}>
<Text
style={[
{ fontSize: 7, padding: 2, borderRadius: 2 },
getStatusStyle(item.latest_approval?.action),
]}
>
{item.latest_approval?.step_name || '-'}
</Text>
</View>
</View>
);
})}
{/* Supplier Subtotal Row */}
<View style={pdfStyles.grandTotalRow}>
<View
style={[
pdfStyles.tableCellNarrow,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[pdfStyles.tableCellWrap, { borderRightWidth: 0 }]}
>
<Text></Text>
</View>
<View
style={[pdfStyles.tableCellWrap, { borderRightWidth: 0 }]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMedium,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMedium,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellXSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellXSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
{/* Pengajuan Subtotal */}
<View
style={[
pdfStyles.tableCellRightXSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text style={{ fontWeight: 'bold' }}>Subtotal</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text style={{ fontWeight: 'bold' }}>
{formatCurrency(supplierTotals.pengajuan)}
</Text>
</View>
{/* Realisasi Subtotal */}
<View
style={[
pdfStyles.tableCellRightXSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text style={{ fontWeight: 'bold' }}>Subtotal</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text style={{ fontWeight: 'bold' }}>
{formatCurrency(supplierTotals.realisasi)}
</Text>
</View>
<View style={pdfStyles.tableCellMedium}>
<Text></Text>
</View>
</View>
</View>
</View>
);
}
)}
{/* Grand Total Section */}
<View style={pdfStyles.allocationSection}>
<View
style={{
width: '30%',
borderTopWidth: 1,
borderTopColor: '#000000',
borderTopStyle: 'solid',
borderLeftWidth: 1,
borderLeftColor: '#000000',
borderLeftStyle: 'solid',
}}
>
<View style={pdfStyles.innerRow}>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
GRAND TOTAL PENGAJUAN
</Text>
</View>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
{formatCurrency(grandTotals.pengajuan)}
</Text>
</View>
</View>
<View style={pdfStyles.innerRow}>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
GRAND TOTAL REALISASI
</Text>
</View>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
{formatCurrency(grandTotals.realisasi)}
</Text>
</View>
</View>
</View>
</View>
{/* Footer */}
<View style={pdfStyles.footer}>
<View style={pdfStyles.footerCompany}>
<Text>PT LUMBUNG TELUR INDONESIA</Text>
</View>
</View>
</Page>
</Document>
);
};
export const generateReportExpensePDF = async ( export const generateReportExpensePDF = async (
data: ReportExpense[], data: ReportExpense[],
params: PDFParams params: PDFParams
): Promise<void> => { ): Promise<void> => {
try { // Inisialisasi dokumen dengan tipe yang sudah diekstensi
const doc = <PDFDocument data={data} params={params} />; const doc = new jsPDF('l', 'mm', 'a4') as jsPDFWithAutoTable;
const blob = await pdf(doc).toBlob(); const pageWidth: number = doc.internal.pageSize.getWidth();
const url = URL.createObjectURL(blob); const marginX: number = 14;
const link = document.createElement('a');
link.href = url; // --- HEADER SECTION ---
link.download = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`; doc.setFont('helvetica', 'bold');
document.body.appendChild(link); doc.setFontSize(18);
link.click(); doc.setTextColor(31, 116, 191); // #1f74bf sesuai style
document.body.removeChild(link); doc.text('PT LUMBUNG TELUR INDONESIA', marginX, 20);
URL.revokeObjectURL(url);
} catch (error) { doc.setFont('helvetica', 'normal');
throw error; doc.setFontSize(7);
doc.setTextColor(102, 102, 102);
doc.text(
'SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Cipedes, Kec. Sukajadi, Kota Bandung 40162',
marginX,
25
);
doc.setDrawColor(0);
doc.line(marginX, 28, pageWidth - marginX, 28);
// --- TITLE & INFO SECTION ---
doc.setFontSize(18);
doc.setTextColor(31, 116, 191);
doc.text('LAPORAN BIAYA OPERASIONAL', marginX, 38);
doc.setFontSize(7);
doc.setTextColor(0);
const infoX: number = pageWidth - marginX;
doc.text(
`Tanggal Cetak: ${formatDate(new Date(), 'DD MMM YYYY')}`,
infoX,
35,
{ align: 'right' }
);
doc.text(`Total Data: ${data.length} transaksi`, infoX, 40, {
align: 'right',
});
// --- GROUPING LOGIC ---
const groupedBySupplier = data.reduce(
(acc, item) => {
const supplierName: string = item.supplier?.name || 'Unknown Supplier';
if (!acc[supplierName]) acc[supplierName] = [];
acc[supplierName].push(item);
return acc;
},
{} as Record<string, ReportExpense[]>
);
let currentY: number = 50;
// --- RENDER TABLES PER SUPPLIER ---
Object.entries(groupedBySupplier).forEach(([supplierName, items]) => {
// Cek sisa ruang halaman sebelum cetak judul supplier
if (currentY > 180) {
doc.addPage();
currentY = 20;
}
doc.setFontSize(14);
doc.setTextColor(31, 116, 191);
doc.text(supplierName, marginX, currentY);
currentY += 5;
const tableOptions: UserOptions = {
startY: currentY,
head: [
[
{ content: 'No', rowSpan: 2 },
{ content: 'No. PO', rowSpan: 2 },
{ content: 'No. Referensi', rowSpan: 2 },
{ content: 'Tgl Realisasi', rowSpan: 2 },
{ content: 'Tgl Transaksi', rowSpan: 2 },
{ content: 'Kategori', rowSpan: 2 },
{ content: 'Produk', rowSpan: 2 },
{ content: 'Lokasi', rowSpan: 2 },
{ content: 'Kandang', rowSpan: 2 },
{ content: 'Pengajuan', colSpan: 3, styles: { halign: 'center' } },
{ content: 'Realisasi', colSpan: 3, styles: { halign: 'center' } },
{ content: 'Status BOP', rowSpan: 2 },
],
['Qty', 'Harga', 'Total', 'Qty', 'Harga', 'Total'],
],
body: items.map((item, index) => {
const pQty: number = item.pengajuan?.qty || 0;
const pPrice: number = item.pengajuan?.price || 0;
const rQty: number = item.realisasi?.qty || 0;
const rPrice: number = item.realisasi?.price || 0;
return [
index + 1,
item.po_number || '-',
item.reference_number || '-',
formatDate(item.realization_date, 'DD MMM YY'),
formatDate(item.transaction_date, 'DD MMM YY'),
item.category?.replace('-', ' ') || '-',
item.pengajuan?.nonstock?.name || '-',
item.kandang?.location?.name || '-',
item.kandang?.name || '-',
pQty.toLocaleString('id-ID'),
formatCurrency(pPrice),
formatCurrency(pQty * pPrice),
rQty.toLocaleString('id-ID'),
formatCurrency(rPrice),
formatCurrency(rQty * rPrice),
item.latest_approval?.step_name || '-',
];
}),
theme: 'grid',
styles: { fontSize: 6, cellPadding: 1.5, overflow: 'linebreak' },
headStyles: {
fillColor: [245, 245, 245],
textColor: 0,
fontStyle: 'bold',
lineWidth: 0.1,
},
// HOOK UNTUK BADGE:
didParseCell: (dataCell) => {
// Index kolom 15 adalah Status BOP (berdasarkan array di atas)
if (dataCell.section === 'body' && dataCell.column.index === 15) {
const statusText = dataCell.cell.raw as string;
// Berikan warna latar belakang sel sesuai status
dataCell.cell.styles.fillColor = getStatusColor(statusText);
dataCell.cell.styles.textColor = [0, 0, 0]; // Teks hitam agar terbaca
dataCell.cell.styles.fontStyle = 'bold';
dataCell.cell.styles.halign = 'center';
}
},
margin: { left: marginX, right: marginX },
};
autoTable(doc, tableOptions);
currentY = doc.lastAutoTable.finalY + 10;
});
// --- GRAND TOTAL SECTION ---
const grandTotals = data.reduce(
(acc, item) => {
const pTotal = (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0);
const rTotal = (item.realisasi?.qty || 0) * (item.realisasi?.price || 0);
return {
pengajuan: acc.pengajuan + pTotal,
realisasi: acc.realisasi + rTotal,
};
},
{ pengajuan: 0, realisasi: 0 }
);
if (currentY > 250) {
doc.addPage();
currentY = 20;
} }
autoTable(doc, {
startY: currentY,
body: [
['GRAND TOTAL PENGAJUAN', formatCurrency(grandTotals.pengajuan)],
['GRAND TOTAL REALISASI', formatCurrency(grandTotals.realisasi)],
],
styles: { fontSize: 8, fontStyle: 'bold' },
columnStyles: {
0: { cellWidth: 50, fillColor: [245, 245, 245] },
1: { cellWidth: 50 },
},
theme: 'grid',
margin: { left: marginX },
});
// --- FOOTER ---
const finalY: number = doc.lastAutoTable.finalY + 20;
doc.setFontSize(14);
doc.setTextColor(31, 116, 191);
doc.text('PT LUMBUNG TELUR INDONESIA', pageWidth - marginX, finalY, {
align: 'right',
});
// Download File
const fileName: string = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
doc.save(fileName);
}; };
File diff suppressed because it is too large Load Diff
-29
View File
@@ -1,29 +0,0 @@
/**
* Dummy data for ReportExpense[]
* Generated from: report-expense.json
*
* This file is auto-generated. Do not edit manually.
*/
import { BaseApiResponse } from '@/types/api/api-general';
import dummyData from './reports-expense.dummy.json';
import { ReportExpense } from '@/types/api/report/report-expense';
/**
* Get dummy ReportExpense[] data
* @returns Promise with BaseApiResponse containing ReportExpense[]
*/
export async function getDummyExpense(): Promise<
BaseApiResponse<ReportExpense[]>
> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
code: 200,
status: 'success',
message: 'Data retrieved successfully',
data: dummyData as unknown as ReportExpense[],
});
}, 500);
});
}
-5
View File
@@ -16,11 +16,6 @@ export class ReportExpenseApiService extends BaseApiService<
async getAllFetcher( async getAllFetcher(
endpoint: string endpoint: string
): Promise<BaseApiResponse<ReportExpense[]>> { ): Promise<BaseApiResponse<ReportExpense[]>> {
// TODO: Remove this block when backend is ready
// const { getDummyExpense } = await import('@/dummy/reports-expense.dummy');
// return await getDummyExpense();
// Uncomment this when backend is ready
return await httpClientFetcher<BaseApiResponse<ReportExpense[]>>(endpoint); return await httpClientFetcher<BaseApiResponse<ReportExpense[]>>(endpoint);
} }
} }