mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'feat/FE/US-352/daily-kandang-hpp-report' into 'development'
[FEAT/FE][US#352] HPP Harian Per Kandang Report See merge request mbugroup/lti-web-client!114
This commit is contained in:
Generated
+16
-97
@@ -28,7 +28,7 @@
|
|||||||
"swr": "^2.3.6",
|
"swr": "^2.3.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"use-debounce": "^10.0.6",
|
"use-debounce": "^10.0.6",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"yup": "^1.7.0",
|
"yup": "^1.7.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
@@ -1871,6 +1871,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -1947,6 +1948,7 @@
|
|||||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.2",
|
"@typescript-eslint/scope-manager": "8.46.2",
|
||||||
"@typescript-eslint/types": "8.46.2",
|
"@typescript-eslint/types": "8.46.2",
|
||||||
@@ -2470,6 +2472,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2487,15 +2490,6 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/adler-32": {
|
|
||||||
"version": "1.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
|
||||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@@ -2966,19 +2960,6 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
"node_modules/cfb": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"adler-32": "~1.3.0",
|
|
||||||
"crc-32": "~1.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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,15 +3001,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/codepage": {
|
|
||||||
"version": "1.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
|
||||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -3111,18 +3083,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/crc-32": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"crc32": "bin/crc32.njs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3158,7 +3118,8 @@
|
|||||||
"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",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/daisyui": {
|
"node_modules/daisyui": {
|
||||||
"version": "5.5.8",
|
"version": "5.5.8",
|
||||||
@@ -3624,6 +3585,7 @@
|
|||||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3797,6 +3759,7 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -4311,15 +4274,6 @@
|
|||||||
"react": ">=16.8.0"
|
"react": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/frac": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -6370,6 +6324,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -6400,6 +6355,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -6974,18 +6930,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ssf": {
|
|
||||||
"version": "0.11.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
|
||||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"frac": "~1.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||||
@@ -7345,6 +7289,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -7512,6 +7457,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -7787,24 +7733,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wmf": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/word": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
|
||||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
@@ -7816,19 +7744,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/xlsx": {
|
"node_modules/xlsx": {
|
||||||
"version": "0.18.5",
|
"version": "0.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
"integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
|
||||||
"adler-32": "~1.3.0",
|
|
||||||
"cfb": "~1.2.1",
|
|
||||||
"codepage": "~1.15.0",
|
|
||||||
"crc-32": "~1.2.1",
|
|
||||||
"ssf": "~0.11.2",
|
|
||||||
"wmf": "~1.0.1",
|
|
||||||
"word": "~0.3.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"xlsx": "bin/xlsx.njs"
|
"xlsx": "bin/xlsx.njs"
|
||||||
},
|
},
|
||||||
@@ -7906,4 +7825,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -31,7 +31,7 @@
|
|||||||
"swr": "^2.3.6",
|
"swr": "^2.3.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"use-debounce": "^10.0.6",
|
"use-debounce": "^10.0.6",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"yup": "^1.7.0",
|
"yup": "^1.7.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
@@ -50,4 +50,4 @@
|
|||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { ReactNode, useState, useRef } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export interface DropdownProps {
|
||||||
|
trigger: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
trigger?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
align?: 'start' | 'center' | 'end';
|
||||||
|
direction?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
hover?: boolean;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
close?: boolean;
|
||||||
|
controlled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dropdown = ({
|
||||||
|
trigger,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
align,
|
||||||
|
direction,
|
||||||
|
hover,
|
||||||
|
defaultOpen = false,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
controlled = false,
|
||||||
|
}: DropdownProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
if (!controlled) {
|
||||||
|
const newState = !isOpen;
|
||||||
|
setIsOpen(newState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWrapperClasses = () => {
|
||||||
|
const openState = controlled ? open : isOpen;
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
'dropdown',
|
||||||
|
{
|
||||||
|
'dropdown-start': align === 'start',
|
||||||
|
'dropdown-center': align === 'center',
|
||||||
|
'dropdown-end': align === 'end',
|
||||||
|
'dropdown-top': direction === 'top',
|
||||||
|
'dropdown-bottom': direction === 'bottom',
|
||||||
|
'dropdown-left': direction === 'left',
|
||||||
|
'dropdown-right': direction === 'right',
|
||||||
|
'dropdown-hover': hover,
|
||||||
|
'dropdown-open': openState && !close,
|
||||||
|
'dropdown-close': close,
|
||||||
|
},
|
||||||
|
className?.wrapper
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTriggerClasses = () => {
|
||||||
|
return cn(className?.trigger);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContentClasses = () => {
|
||||||
|
return cn(
|
||||||
|
'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box',
|
||||||
|
className?.content
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (controlled) {
|
||||||
|
return (
|
||||||
|
<div className={getWrapperClasses()}>
|
||||||
|
{trigger}
|
||||||
|
{open && !close && (
|
||||||
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={dropdownRef} className={getWrapperClasses()}>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
className={getTriggerClasses()}
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleDropdown();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trigger}
|
||||||
|
</div>
|
||||||
|
{!close && (
|
||||||
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
@@ -7,7 +7,7 @@ import { Icon } from '@iconify/react';
|
|||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { AuthApi } from '@/services/api/auth';
|
import { AuthApi } from '@/services/api/auth';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { JSX, useState } from 'react';
|
|||||||
|
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent';
|
import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent';
|
||||||
|
import HppPerKandangTab from './sale/tab/HppPerKandangTab';
|
||||||
|
|
||||||
type MarketingReportTabType =
|
type MarketingReportTabType =
|
||||||
| 'daily'
|
| 'daily'
|
||||||
@@ -21,6 +22,11 @@ const marketingReportTabs: {
|
|||||||
label: 'Penjualan Harian',
|
label: 'Penjualan Harian',
|
||||||
content: <DailyMarketingReportContent />,
|
content: <DailyMarketingReportContent />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'daily-hpp',
|
||||||
|
label: 'HPP Harian Kandang',
|
||||||
|
content: <HppPerKandangTab />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const MarketingReportContent = () => {
|
const MarketingReportContent = () => {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Tabs from '@/components/Tabs';
|
||||||
|
import HppPerKandangTab from '@/components/pages/report/sale/tab/HppPerKandangTab';
|
||||||
|
|
||||||
|
const SaleReportTabs = () => {
|
||||||
|
const tabs = [
|
||||||
|
// {
|
||||||
|
// id: '1',
|
||||||
|
// label: 'Penjualan Harian',
|
||||||
|
// content: 'Penjualan Harian Tab',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '2',
|
||||||
|
// label: 'Transaksi Penjualan DO',
|
||||||
|
// content: 'Transaksi Penjualan DO Tab',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '3',
|
||||||
|
// label: 'Perbandingan HPP Per Rentang BW',
|
||||||
|
// content: 'Perbandingan HPP Per Rentang BW Tab',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
label: 'HPP Harian Kandang',
|
||||||
|
content: <HppPerKandangTab />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<Tabs tabs={tabs} variant='lifted' />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SaleReportTabs;
|
||||||
@@ -0,0 +1,497 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
Document,
|
||||||
|
StyleSheet,
|
||||||
|
Font,
|
||||||
|
pdf,
|
||||||
|
} from '@react-pdf/renderer';
|
||||||
|
import {
|
||||||
|
HppPerKandangReport,
|
||||||
|
HppPerKandangRow,
|
||||||
|
HppPerKandangPerWeightRange,
|
||||||
|
} from '@/types/api/report/hpp-per-kandang';
|
||||||
|
import { formatDate, formatNumber, formatCurrency } from '@/lib/helper';
|
||||||
|
|
||||||
|
Font.register({
|
||||||
|
family: 'Helvetica',
|
||||||
|
src: 'helvetica',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfStyles = StyleSheet.create({
|
||||||
|
page: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: 'Helvetica',
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
titleSection: {
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
mainTitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 5,
|
||||||
|
color: '#1f74bf',
|
||||||
|
},
|
||||||
|
supplierTitle: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 8,
|
||||||
|
color: '#1f74bf',
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#000000',
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
tableRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
tableHeader: {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
},
|
||||||
|
tableCell: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
|
tableCellHeader: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000000',
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
paddingVertical: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
tableCellHeaderRight: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
textAlign: 'right',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000000',
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
tableCellRight: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
tableCellCenter: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
tableBorderBottom: {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000000',
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
},
|
||||||
|
supplierSection: {
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
supplierSectionBreak: {
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
parameterBadge: {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
color: '#333333',
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
marginRight: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
parameterContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface HppPerKandangExportParams {
|
||||||
|
data: HppPerKandangReport;
|
||||||
|
params: {
|
||||||
|
area_name?: string;
|
||||||
|
location_name?: string;
|
||||||
|
kandang_name?: string;
|
||||||
|
period?: string;
|
||||||
|
weight_min?: string;
|
||||||
|
weight_max?: string;
|
||||||
|
show_unrecorded?: string;
|
||||||
|
sort_by?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getParameterText = (params: HppPerKandangExportParams['params']) => {
|
||||||
|
const paramsText = [];
|
||||||
|
|
||||||
|
if (params.area_name && params.area_name !== 'Semua Area') {
|
||||||
|
paramsText.push(`Area: ${params.area_name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.location_name && params.location_name !== 'Semua Lokasi') {
|
||||||
|
paramsText.push(`Lokasi: ${params.location_name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.kandang_name && params.kandang_name !== 'Semua Kandang') {
|
||||||
|
paramsText.push(`Kandang: ${params.kandang_name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.period) {
|
||||||
|
const formattedDate = formatDate(params.period, 'DD MMM YYYY');
|
||||||
|
paramsText.push(`Tanggal: ${formattedDate}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.weight_min || params.weight_max) {
|
||||||
|
const weightRange =
|
||||||
|
params.weight_min && params.weight_max
|
||||||
|
? `${params.weight_min} - ${params.weight_max} kg`
|
||||||
|
: params.weight_min
|
||||||
|
? `≥ ${params.weight_min} kg`
|
||||||
|
: `≤ ${params.weight_max} kg`;
|
||||||
|
paramsText.push(`Rentang Bobot: ${weightRange}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.show_unrecorded === 'true') {
|
||||||
|
paramsText.push('Tampilkan: Tanpa Recording');
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm');
|
||||||
|
paramsText.push(`Dicetak: ${currentDate}`);
|
||||||
|
|
||||||
|
return paramsText;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPDFDocument = (
|
||||||
|
data: HppPerKandangExportParams['data'],
|
||||||
|
params: HppPerKandangExportParams['params']
|
||||||
|
) => {
|
||||||
|
const rekapitulasiByWeightRange = data.summary?.per_weight_range || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
||||||
|
{/* Title and Parameters */}
|
||||||
|
<View style={pdfStyles.titleSection}>
|
||||||
|
<Text style={pdfStyles.mainTitle}>
|
||||||
|
Laporan > HPP Harian Kandang
|
||||||
|
</Text>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
{getParameterText(params).map((param, index) => (
|
||||||
|
<View key={index} style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>{param}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Rekapitulasi Section */}
|
||||||
|
<View style={pdfStyles.supplierSection}>
|
||||||
|
<Text style={pdfStyles.supplierTitle}>Rekapitulasi</Text>
|
||||||
|
|
||||||
|
<View style={pdfStyles.table}>
|
||||||
|
{/* Table Header */}
|
||||||
|
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
|
<Text>Rentang BW</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Sisa Ekor</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Sisa Kg</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Produksi Telur (Butir)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Produksi Telur (Kg)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
|
<Text>Feed (Supplier)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
|
<Text>DOC (Supplier)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Rata-Rata Harga DOC</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Nilai Nominal Telur</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>HPP Ayam</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>HPP Telur (RP/KG)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Nominal Sisa</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table Body - Rekapitulasi */}
|
||||||
|
{rekapitulasiByWeightRange.map(
|
||||||
|
(group: HppPerKandangPerWeightRange, index: number) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableRow,
|
||||||
|
index < rekapitulasiByWeightRange.length - 1
|
||||||
|
? pdfStyles.tableBorderBottom
|
||||||
|
: {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
||||||
|
<Text>{group.label}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(group.remaining_chicken_birds)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>
|
||||||
|
{formatNumber(group.remaining_chicken_weight_kg)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatNumber(group.avg_weight_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(group.egg_production_pieces)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(group.egg_production_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
|
<Text>
|
||||||
|
{group.feed_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
|
<Text>
|
||||||
|
{group.doc_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(group.average_doc_price_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(group.egg_value_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatCurrency(group.hpp_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(group.remaining_value_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Detail Per Kandang Section */}
|
||||||
|
<View style={pdfStyles.supplierSectionBreak}>
|
||||||
|
<Text style={pdfStyles.supplierTitle}>Detail Per Kandang</Text>
|
||||||
|
|
||||||
|
<View style={pdfStyles.table}>
|
||||||
|
{/* Table Header */}
|
||||||
|
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
|
||||||
|
<Text>No</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
|
<Text>Kandang</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
|
<Text>Rentang BW</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>Sisa Ekor</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>Sisa Kg (Ayam)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>Produksi Telur (Butir)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>Produksi Telur (Kg)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
|
<Text>Feed (Supplier)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
|
<Text>DOC (Supplier)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Rata-Rata Harga DOC</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Nilai Nominal Telur</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>HPP Ayam</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>HPP Telur (RP/KG)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Nominal Sisa</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table Body - Detail Per Kandang */}
|
||||||
|
{data.rows.map((item: HppPerKandangRow, index: number) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableRow,
|
||||||
|
index < data.rows.length - 1
|
||||||
|
? pdfStyles.tableBorderBottom
|
||||||
|
: {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
|
||||||
|
<Text>{index + 1}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
|
<Text>{item.kandang?.name || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
|
<Text>
|
||||||
|
{item.weight_range.weight_min.toFixed(2)} -{' '}
|
||||||
|
{item.weight_range.weight_max.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(item.avg_weight_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatNumber(item.remaining_chicken_birds)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatNumber(item.remaining_chicken_weight_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatNumber(item.egg_production_pieces)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatNumber(item.egg_production_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
|
<Text>
|
||||||
|
{item.feed_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
|
<Text>
|
||||||
|
{item.doc_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(item.average_doc_price_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(item.egg_value_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatCurrency(item.hpp_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(item.remaining_value_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateHppPerKandangPDF = async (
|
||||||
|
data: HppPerKandangExportParams['data'],
|
||||||
|
params: HppPerKandangExportParams['params']
|
||||||
|
): Promise<void> => {
|
||||||
|
const PDFDocument = createPDFDocument(data, params);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await pdf(PDFDocument).toBlob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
const period = params.period || formatDate(new Date(), 'YYYY-MM-DD');
|
||||||
|
link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,959 @@
|
|||||||
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
|
import { ChangeEventHandler } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import SelectInput, {
|
||||||
|
useSelect,
|
||||||
|
OptionType,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
|
import { SaleReportApi } from '@/services/api/report/marketing-sale';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import { ColumnDef, Row, flexRender } from '@tanstack/react-table';
|
||||||
|
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
|
import {
|
||||||
|
HppPerKandangReport,
|
||||||
|
HppPerKandangRow,
|
||||||
|
HppPerKandangPerWeightRange,
|
||||||
|
} from '@/types/api/report/hpp-per-kandang';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
|
import Menu from '@/components/menu/Menu';
|
||||||
|
import { generateHppPerKandangPDF } from '../export/HppPerkandangExport';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
const HppPerKandangTab = () => {
|
||||||
|
// ===== STATE MANAGEMENT =====
|
||||||
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||||
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||||
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||||
|
|
||||||
|
// ===== SUBMISSION STATE =====
|
||||||
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
|
// ===== TABLE FILTER STATE =====
|
||||||
|
const { state: tableFilterState, updateFilter } = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
area_id: [] as string[],
|
||||||
|
location_id: [] as string[],
|
||||||
|
kandang_id: [] as string[],
|
||||||
|
weight_min: '',
|
||||||
|
weight_max: '',
|
||||||
|
period: '',
|
||||||
|
sort_by: '',
|
||||||
|
show_unrecorded: false,
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
||||||
|
AreaApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
|
||||||
|
useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
|
||||||
|
useSelect(KandangApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const showUnrecordedOptions: OptionType[] = [
|
||||||
|
{ value: 'false', label: 'Sembunyikan' },
|
||||||
|
{ value: 'true', label: 'Tampilkan' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const areaChangeHandler = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||||
|
updateFilter(
|
||||||
|
'area_id',
|
||||||
|
arr.map((v) => String((v as OptionType).value))
|
||||||
|
);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const locationChangeHandler = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||||
|
updateFilter(
|
||||||
|
'location_id',
|
||||||
|
arr.map((v) => String((v as OptionType).value))
|
||||||
|
);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const kandangChangeHandler = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||||
|
updateFilter(
|
||||||
|
'kandang_id',
|
||||||
|
arr.map((v) => String((v as OptionType).value))
|
||||||
|
);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const weightMinChangeHandler = useCallback<
|
||||||
|
ChangeEventHandler<HTMLInputElement>
|
||||||
|
>(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
updateFilter('weight_min', val ? String(parseFloat(val) || 0) : '');
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const weightMaxChangeHandler = useCallback<
|
||||||
|
ChangeEventHandler<HTMLInputElement>
|
||||||
|
>(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
updateFilter('weight_max', val ? String(parseFloat(val) || 0) : '');
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
updateFilter('period', val || '');
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const showUnrecordedChangeHandler = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newVal = val as OptionType;
|
||||||
|
updateFilter('show_unrecorded', newVal?.value === 'true');
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetFilters = useCallback(() => {
|
||||||
|
updateFilter('area_id', []);
|
||||||
|
updateFilter('location_id', []);
|
||||||
|
updateFilter('kandang_id', []);
|
||||||
|
updateFilter('weight_min', '');
|
||||||
|
updateFilter('weight_max', '');
|
||||||
|
updateFilter('period', '');
|
||||||
|
updateFilter('sort_by', '');
|
||||||
|
updateFilter('show_unrecorded', false);
|
||||||
|
setIsSubmitted(false);
|
||||||
|
}, [updateFilter]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
if (!tableFilterState.period) {
|
||||||
|
toast.error('Periode wajib diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitted(true);
|
||||||
|
}, [tableFilterState.period]);
|
||||||
|
|
||||||
|
// ===== DATA FETCHING =====
|
||||||
|
const { data: hppPerKandang, isLoading } = useSWR(
|
||||||
|
isSubmitted
|
||||||
|
? () => {
|
||||||
|
const params = {
|
||||||
|
area_id:
|
||||||
|
tableFilterState.area_id.length > 0
|
||||||
|
? tableFilterState.area_id.join(',')
|
||||||
|
: undefined,
|
||||||
|
location_id:
|
||||||
|
tableFilterState.location_id.length > 0
|
||||||
|
? tableFilterState.location_id.join(',')
|
||||||
|
: undefined,
|
||||||
|
kandang_id:
|
||||||
|
tableFilterState.kandang_id.length > 0
|
||||||
|
? tableFilterState.kandang_id.join(',')
|
||||||
|
: undefined,
|
||||||
|
weight_min: tableFilterState.weight_min || undefined,
|
||||||
|
weight_max: tableFilterState.weight_max || undefined,
|
||||||
|
period: tableFilterState.period || undefined,
|
||||||
|
sort_by: tableFilterState.sort_by || undefined,
|
||||||
|
show_unrecorded: tableFilterState.show_unrecorded,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ['hpp-per-kandang-report', params];
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
([, params]) =>
|
||||||
|
SaleReportApi.getHppPerKandangReport(
|
||||||
|
params.area_id,
|
||||||
|
params.location_id,
|
||||||
|
params.kandang_id,
|
||||||
|
params.weight_min,
|
||||||
|
params.weight_max,
|
||||||
|
params.period,
|
||||||
|
params.sort_by,
|
||||||
|
params.show_unrecorded
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: HppPerKandangReport['rows'] = useMemo(
|
||||||
|
() =>
|
||||||
|
isResponseSuccess(hppPerKandang)
|
||||||
|
? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || []
|
||||||
|
: [],
|
||||||
|
[hppPerKandang]
|
||||||
|
);
|
||||||
|
|
||||||
|
const summaryTotal =
|
||||||
|
isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary?.total
|
||||||
|
? hppPerKandang.data.summary.total
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const perWeightRangeSummary = useMemo(
|
||||||
|
() =>
|
||||||
|
isResponseSuccess(hppPerKandang) &&
|
||||||
|
hppPerKandang?.data?.summary?.per_weight_range
|
||||||
|
? hppPerKandang.data.summary.per_weight_range
|
||||||
|
: [],
|
||||||
|
[hppPerKandang]
|
||||||
|
);
|
||||||
|
|
||||||
|
const period =
|
||||||
|
isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period
|
||||||
|
? hppPerKandang.data.period
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// ===== EXPORT DATA FETCHER =====
|
||||||
|
const hppPerKandangExport =
|
||||||
|
useCallback(async (): Promise<HppPerKandangReport | null> => {
|
||||||
|
const params = {
|
||||||
|
area_id:
|
||||||
|
tableFilterState.area_id.length > 0
|
||||||
|
? tableFilterState.area_id.join(',')
|
||||||
|
: undefined,
|
||||||
|
location_id:
|
||||||
|
tableFilterState.location_id.length > 0
|
||||||
|
? tableFilterState.location_id.join(',')
|
||||||
|
: undefined,
|
||||||
|
kandang_id:
|
||||||
|
tableFilterState.kandang_id.length > 0
|
||||||
|
? tableFilterState.kandang_id.join(',')
|
||||||
|
: undefined,
|
||||||
|
weight_min: tableFilterState.weight_min || undefined,
|
||||||
|
weight_max: tableFilterState.weight_max || undefined,
|
||||||
|
period: tableFilterState.period || undefined,
|
||||||
|
sort_by: tableFilterState.sort_by || undefined,
|
||||||
|
show_unrecorded: tableFilterState.show_unrecorded,
|
||||||
|
limit: 10000,
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await SaleReportApi.getHppPerKandangReport(
|
||||||
|
params.area_id,
|
||||||
|
params.location_id,
|
||||||
|
params.kandang_id,
|
||||||
|
params.weight_min,
|
||||||
|
params.weight_max,
|
||||||
|
params.period,
|
||||||
|
params.sort_by,
|
||||||
|
params.show_unrecorded
|
||||||
|
);
|
||||||
|
|
||||||
|
return isResponseSuccess(response) ? response.data : null;
|
||||||
|
}, [tableFilterState]);
|
||||||
|
|
||||||
|
// ===== TABLE COLUMNS DEFINITION =====
|
||||||
|
const allFeedSuppliers = useMemo(() => {
|
||||||
|
const suppliers = new Set<string>();
|
||||||
|
data.forEach((item: HppPerKandangRow) => {
|
||||||
|
item.feed_suppliers?.forEach((s: { alias?: string; name: string }) => {
|
||||||
|
suppliers.add(s.alias || s.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(suppliers).join(' | ');
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const allDocSuppliers = useMemo(() => {
|
||||||
|
const suppliers = new Set<string>();
|
||||||
|
data.forEach((item: HppPerKandangRow) => {
|
||||||
|
item.doc_suppliers?.forEach((s: { alias?: string; name: string }) => {
|
||||||
|
suppliers.add(s.alias || s.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(suppliers).join(' | ');
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// ===== EXPORT HANDLERS =====
|
||||||
|
const handleExportExcel = useCallback(async () => {
|
||||||
|
setIsExcelExportLoading(true);
|
||||||
|
try {
|
||||||
|
const allDataForExport = await hppPerKandangExport();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allDataForExport ||
|
||||||
|
!allDataForExport?.rows ||
|
||||||
|
allDataForExport.rows.length === 0
|
||||||
|
) {
|
||||||
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allExportData =
|
||||||
|
allDataForExport.rows as HppPerKandangReport['rows'];
|
||||||
|
|
||||||
|
const summaryTotal = allDataForExport.summary.total;
|
||||||
|
|
||||||
|
const excelData: { [key: string]: string | number }[] = allExportData.map(
|
||||||
|
(item: HppPerKandangRow, index: number) => ({
|
||||||
|
No: index + 1,
|
||||||
|
Kandang: item.kandang?.name || '',
|
||||||
|
'Rentang Bobot': item.weight_range
|
||||||
|
? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}`
|
||||||
|
: '',
|
||||||
|
'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0,
|
||||||
|
'Sisa Ayam (Ekor)': item.remaining_chicken_birds || 0,
|
||||||
|
'Sisa Ayam (KG)': item.remaining_chicken_weight_kg || 0,
|
||||||
|
'Produksi Telur (Butir)': item.egg_production_pieces || 0,
|
||||||
|
'Produksi Telur (KG)': item.egg_production_kg || 0,
|
||||||
|
'Feed (Supplier)':
|
||||||
|
item.feed_suppliers
|
||||||
|
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
||||||
|
.join(' | ') || '',
|
||||||
|
'DOC (Supplier)':
|
||||||
|
item.doc_suppliers
|
||||||
|
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
||||||
|
.join(' | ') || '',
|
||||||
|
'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0,
|
||||||
|
'Nilai Nominal Telur (RP)': item.egg_value_rp || 0,
|
||||||
|
'HPP Ayam (RP)': item.hpp_rp || 0,
|
||||||
|
'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0,
|
||||||
|
'Nilai Nominal Sisa Ayam (RP)': item.remaining_value_rp || 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
excelData.push({
|
||||||
|
No: 'TOTAL',
|
||||||
|
Kandang: 'ALL',
|
||||||
|
'Rentang Bobot': '-',
|
||||||
|
'Rata-Rata Bobot (KG)': summaryTotal?.average_weight_kg || 0,
|
||||||
|
'Sisa Ayam (Ekor)': summaryTotal?.total_remaining_chicken_birds || 0,
|
||||||
|
'Sisa Ayam (KG)': summaryTotal?.total_remaining_chicken_weight_kg || 0,
|
||||||
|
'Produksi Telur (Butir)':
|
||||||
|
summaryTotal?.total_egg_production_pieces || 0,
|
||||||
|
'Produksi Telur (KG)': summaryTotal?.total_egg_production_kg || 0,
|
||||||
|
'Feed (Supplier)': allFeedSuppliers,
|
||||||
|
'DOC (Supplier)': allDocSuppliers,
|
||||||
|
'Rata-Rata Harga DOC (RP)':
|
||||||
|
summaryTotal?.total_average_doc_price_rp || 0,
|
||||||
|
'Nilai Nominal Telur (RP)': summaryTotal?.total_egg_value_rp || 0,
|
||||||
|
'HPP Ayam (RP)': summaryTotal?.total_hpp_rp || 0,
|
||||||
|
'HPP Telur (RP/KG)': summaryTotal?.average_egg_hpp_rp_per_kg || 0,
|
||||||
|
'Nilai Nominal Sisa Ayam (RP)':
|
||||||
|
summaryTotal?.total_remaining_value_rp || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
||||||
|
|
||||||
|
const colWidths = [
|
||||||
|
{ wch: 5 }, // No
|
||||||
|
{ wch: 30 }, // Kandang
|
||||||
|
{ wch: 15 }, // Rentang Bobot
|
||||||
|
{ wch: 18 }, // Rata-Rata Bobot (KG)
|
||||||
|
{ wch: 15 }, // Sisa Ayam (Ekor)
|
||||||
|
{ wch: 15 }, // Sisa Ayam (KG)
|
||||||
|
{ wch: 18 }, // Produksi Telur (Butir)
|
||||||
|
{ wch: 18 }, // Produksi Telur (KG)
|
||||||
|
{ wch: 20 }, // Feed (Supplier)
|
||||||
|
{ wch: 20 }, // DOC (Supplier)
|
||||||
|
{ wch: 20 }, // Rata-Rata Harga DOC (RP)
|
||||||
|
{ wch: 20 }, // Nilai Nominal Telur (RP)
|
||||||
|
{ wch: 15 }, // HPP Ayam (RP)
|
||||||
|
{ wch: 18 }, // HPP Telur (RP/KG)
|
||||||
|
{ wch: 25 }, // Nilai Nominal Sisa Ayam (RP)
|
||||||
|
];
|
||||||
|
worksheet['!cols'] = colWidths;
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang');
|
||||||
|
|
||||||
|
const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`;
|
||||||
|
|
||||||
|
XLSX.writeFile(workbook, filename);
|
||||||
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
|
} finally {
|
||||||
|
setIsExcelExportLoading(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
hppPerKandangExport,
|
||||||
|
tableFilterState,
|
||||||
|
areaOptions,
|
||||||
|
locationOptions,
|
||||||
|
kandangOptions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleExportPDF = useCallback(async () => {
|
||||||
|
setIsPdfExportLoading(true);
|
||||||
|
try {
|
||||||
|
const allDataForExport = await hppPerKandangExport();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allDataForExport ||
|
||||||
|
!allDataForExport?.rows ||
|
||||||
|
allDataForExport.rows.length === 0
|
||||||
|
) {
|
||||||
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const areaName =
|
||||||
|
tableFilterState.area_id.length > 0
|
||||||
|
? tableFilterState.area_id
|
||||||
|
.map(
|
||||||
|
(id) =>
|
||||||
|
areaOptions.find((opt) => opt.value === Number(id))?.label
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ') || 'Semua Area'
|
||||||
|
: 'Semua Area';
|
||||||
|
|
||||||
|
const locationName =
|
||||||
|
tableFilterState.location_id.length > 0
|
||||||
|
? tableFilterState.location_id
|
||||||
|
.map(
|
||||||
|
(id) =>
|
||||||
|
locationOptions.find((opt) => opt.value === Number(id))?.label
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ') || 'Semua Lokasi'
|
||||||
|
: 'Semua Lokasi';
|
||||||
|
|
||||||
|
const kandangName =
|
||||||
|
tableFilterState.kandang_id.length > 0
|
||||||
|
? tableFilterState.kandang_id
|
||||||
|
.map(
|
||||||
|
(id) =>
|
||||||
|
kandangOptions.find((opt) => opt.value === Number(id))?.label
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ') || 'Semua Kandang'
|
||||||
|
: 'Semua Kandang';
|
||||||
|
|
||||||
|
await generateHppPerKandangPDF(allDataForExport, {
|
||||||
|
area_name: areaName,
|
||||||
|
location_name: locationName,
|
||||||
|
kandang_name: kandangName,
|
||||||
|
period: tableFilterState.period,
|
||||||
|
weight_min: tableFilterState.weight_min,
|
||||||
|
weight_max: tableFilterState.weight_max,
|
||||||
|
show_unrecorded: tableFilterState.show_unrecorded.toString(),
|
||||||
|
sort_by: tableFilterState.sort_by,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||||
|
} finally {
|
||||||
|
setIsPdfExportLoading(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
hppPerKandangExport,
|
||||||
|
tableFilterState,
|
||||||
|
areaOptions,
|
||||||
|
locationOptions,
|
||||||
|
kandangOptions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => {
|
||||||
|
const tableColumns: ColumnDef<HppPerKandangReport['rows'][0]>[] = [
|
||||||
|
{
|
||||||
|
id: 'no',
|
||||||
|
header: 'No',
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
footer: () => <div className='font-semibold text-gray-900'>TOTAL</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kandang_name',
|
||||||
|
header: 'Kandang',
|
||||||
|
accessorKey: 'kandang.name',
|
||||||
|
cell: (props) => {
|
||||||
|
const kandang = props.row.original.kandang;
|
||||||
|
return kandang?.name || '-';
|
||||||
|
},
|
||||||
|
footer: () => <div className='font-semibold text-gray-900'>ALL</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'weight_range',
|
||||||
|
header: 'Rentang Bobot',
|
||||||
|
accessorKey: 'weight_range',
|
||||||
|
cell: (props) => {
|
||||||
|
const weightRange = props.row.original.weight_range;
|
||||||
|
return weightRange
|
||||||
|
? `${formatNumber(weightRange.weight_min)} - ${formatNumber(weightRange.weight_max)}`
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
footer: () => <div className='font-semibold text-gray-900'>-</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'avg_weight_kg',
|
||||||
|
header: 'Rata-Rata Bobot (KG)',
|
||||||
|
accessorKey: 'avg_weight_kg',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.avg_weight_kg;
|
||||||
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.average_weight_kg || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remaining_chicken_birds',
|
||||||
|
header: 'Sisa Ayam (Ekor)',
|
||||||
|
accessorKey: 'remaining_chicken_birds',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.remaining_chicken_birds;
|
||||||
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.total_remaining_chicken_birds || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remaining_chicken_weight_kg',
|
||||||
|
header: 'Sisa Ayam (KG)',
|
||||||
|
accessorKey: 'remaining_chicken_weight_kg',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.remaining_chicken_weight_kg;
|
||||||
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_production_pieces',
|
||||||
|
header: 'Produksi Telur (Butir)',
|
||||||
|
accessorKey: 'egg_production_pieces',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_production_pieces;
|
||||||
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.total_egg_production_pieces || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_production_kg',
|
||||||
|
header: 'Produksi Telur (KG)',
|
||||||
|
accessorKey: 'egg_production_kg',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_production_kg;
|
||||||
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'feed_suppliers',
|
||||||
|
header: 'Feed (Supplier)',
|
||||||
|
accessorKey: 'feed_suppliers',
|
||||||
|
cell: (props) => {
|
||||||
|
const suppliers = props.row.original.feed_suppliers;
|
||||||
|
return (
|
||||||
|
suppliers
|
||||||
|
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
||||||
|
.join(' | ') || '-'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='font-semibold text-gray-900'>
|
||||||
|
{allFeedSuppliers || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'doc_suppliers',
|
||||||
|
header: 'DOC (Supplier)',
|
||||||
|
accessorKey: 'doc_suppliers',
|
||||||
|
cell: (props) => {
|
||||||
|
const suppliers = props.row.original.doc_suppliers;
|
||||||
|
return (
|
||||||
|
suppliers
|
||||||
|
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
||||||
|
.join(' | ') || '-'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='font-semibold text-gray-900'>
|
||||||
|
{allDocSuppliers || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'average_doc_price_rp',
|
||||||
|
header: 'Rata-Rata Harga DOC (RP)',
|
||||||
|
accessorKey: 'average_doc_price_rp',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.average_doc_price_rp;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.total_average_doc_price_rp || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_value_rp',
|
||||||
|
header: 'Nilai Nominal Telur (RP)',
|
||||||
|
accessorKey: 'egg_value_rp',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_value_rp;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.total_egg_value_rp || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hpp_rp',
|
||||||
|
header: 'HPP Ayam (RP)',
|
||||||
|
accessorKey: 'hpp_rp',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.hpp_rp;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.total_hpp_rp || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_hpp_rp_per_kg',
|
||||||
|
header: 'HPP Telur (RP/KG)',
|
||||||
|
accessorKey: 'egg_hpp_rp_per_kg',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_hpp_rp_per_kg;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remaining_value_rp',
|
||||||
|
header: 'Nilai Nominal Sisa Ayam (RP)',
|
||||||
|
accessorKey: 'remaining_value_rp',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.remaining_value_rp;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.total_remaining_value_rp || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return tableColumns;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== CUSTOM ROW RENDERER =====
|
||||||
|
const renderCustomRow = useCallback(
|
||||||
|
(row: Row<HppPerKandangReport['rows'][0]>) => {
|
||||||
|
if (row.index === data.length - 1) {
|
||||||
|
const defaultRow = (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border-gray-200'
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
|
const customRows = [
|
||||||
|
<tr
|
||||||
|
className='border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||||
|
key={'rekapitulasi-row'}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
colSpan={15}
|
||||||
|
className='px-4 py-3 text-gray-900 text-center font-semibold'
|
||||||
|
>
|
||||||
|
Rekapitulasi per rentang bobot
|
||||||
|
</td>
|
||||||
|
</tr>,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (perWeightRangeSummary.length > 0) {
|
||||||
|
perWeightRangeSummary.forEach(
|
||||||
|
(item: HppPerKandangPerWeightRange, index = 0) => {
|
||||||
|
customRows.push(
|
||||||
|
<tr
|
||||||
|
key={`summary-${item.id}`}
|
||||||
|
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200 [&_td]:px-4 [&_td]:py-3 [&_td]:text-xs [&_td]:text-gray-900 [&_td]:whitespace-nowrap'
|
||||||
|
>
|
||||||
|
<td className=''>{index + 1}</td>
|
||||||
|
<td className=''>ALL</td>
|
||||||
|
<td className=''>{item.label}</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.avg_weight_kg)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.remaining_chicken_birds)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.remaining_chicken_weight_kg)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.egg_production_pieces)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.egg_production_kg)}
|
||||||
|
</td>
|
||||||
|
<td className=''>
|
||||||
|
{item.feed_suppliers
|
||||||
|
?.map((s) => s.alias || s.name)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</td>
|
||||||
|
<td className=''>
|
||||||
|
{item.doc_suppliers
|
||||||
|
?.map((s) => s.alias || s.name)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatCurrency(item.average_doc_price_rp)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatCurrency(item.egg_value_rp)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>{formatCurrency(item.hpp_rp)}</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatCurrency(item.egg_hpp_rp_per_kg)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatCurrency(item.remaining_value_rp)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [defaultRow, ...customRows];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[data, perWeightRangeSummary]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-0 sm:p-4'>
|
||||||
|
<Card
|
||||||
|
subtitle={
|
||||||
|
period
|
||||||
|
? `Laporan > HPP Harian Kandang (${period})`
|
||||||
|
: 'Laporan > HPP Harian Kandang'
|
||||||
|
}
|
||||||
|
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
||||||
|
>
|
||||||
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
isMulti
|
||||||
|
options={areaOptions}
|
||||||
|
value={areaOptions.filter((opt) =>
|
||||||
|
(tableFilterState.area_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={areaChangeHandler}
|
||||||
|
isLoading={isLoadingAreas}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
isMulti
|
||||||
|
options={locationOptions}
|
||||||
|
value={locationOptions.filter((opt) =>
|
||||||
|
(tableFilterState.location_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={locationChangeHandler}
|
||||||
|
isLoading={isLoadingLocations}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
isMulti
|
||||||
|
options={kandangOptions}
|
||||||
|
value={kandangOptions.filter((opt) =>
|
||||||
|
(tableFilterState.kandang_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={kandangChangeHandler}
|
||||||
|
isLoading={isLoadingKandangs}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||||
|
<div className='flex flex-row gap-4'>
|
||||||
|
<NumberInput
|
||||||
|
label='Rentang Bobot Min (Kg)'
|
||||||
|
name='weight_min'
|
||||||
|
placeholder='Masukkan bobot minimum'
|
||||||
|
value={tableFilterState.weight_min}
|
||||||
|
onChange={weightMinChangeHandler}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label='Rentang Bobot Max (Kg)'
|
||||||
|
name='weight_max'
|
||||||
|
placeholder='Masukkan bobot maximum'
|
||||||
|
value={tableFilterState.weight_max}
|
||||||
|
onChange={weightMaxChangeHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DateInput
|
||||||
|
label='Periode'
|
||||||
|
name='period'
|
||||||
|
placeholder='Pilih Periode'
|
||||||
|
value={tableFilterState.period}
|
||||||
|
onChange={periodChangeHandler}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Tampilkan Kandang Tanpa Recording'
|
||||||
|
placeholder='Pilih Opsi'
|
||||||
|
options={showUnrecordedOptions}
|
||||||
|
value={
|
||||||
|
tableFilterState.show_unrecorded
|
||||||
|
? showUnrecordedOptions.find((opt) => opt.value === 'true') ||
|
||||||
|
null
|
||||||
|
: showUnrecordedOptions.find((opt) => opt.value === 'false') ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
onChange={showUnrecordedChangeHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||||
|
<Button color='primary' onClick={handleSubmit}>
|
||||||
|
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||||
|
Cari
|
||||||
|
</Button>
|
||||||
|
<Button color='warning' onClick={resetFilters}>
|
||||||
|
<Icon icon='heroicons-outline:refresh' width={20} height={20} />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
trigger={
|
||||||
|
<Button color='success' isLoading={isAnyExportLoading}>
|
||||||
|
Export
|
||||||
|
<Icon
|
||||||
|
icon='heroicons-outline:download'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
align='end'
|
||||||
|
>
|
||||||
|
<Menu className='w-32'>
|
||||||
|
<MenuItem title='Excel' onClick={handleExportExcel} />
|
||||||
|
<MenuItem title='PDF' onClick={handleExportPDF} />
|
||||||
|
</Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='divider'></div>
|
||||||
|
|
||||||
|
{!isSubmitted ? (
|
||||||
|
<div className='mt-6 text-center text-gray-500'>
|
||||||
|
Silakan pilih filter dan klik tombol Cari untuk menampilkan data.
|
||||||
|
</div>
|
||||||
|
) : isLoading ? (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className='mt-6 text-center text-gray-500'>
|
||||||
|
Tidak ada data yang dapat ditampilkan...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table
|
||||||
|
data={data}
|
||||||
|
columns={getTableColumns()}
|
||||||
|
renderFooter={data.length > 0}
|
||||||
|
renderCustomRow={renderCustomRow}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'w-full mt-6',
|
||||||
|
tableWrapperClassName: 'overflow-x-auto mt-4',
|
||||||
|
tableClassName: 'w-full table-auto text-sm',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||||
|
bodyRowClassName:
|
||||||
|
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
|
tableFooterClassName:
|
||||||
|
'bg-gray-100 font-semibold border border-gray-200',
|
||||||
|
footerRowClassName: 'border-t-2 border-gray-300',
|
||||||
|
footerColumnClassName:
|
||||||
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HppPerKandangTab;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { BaseApiService } from '@/services/api/base';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang';
|
||||||
|
|
||||||
|
export class MarketingSaleReportService extends BaseApiService<
|
||||||
|
HppPerKandangReport,
|
||||||
|
unknown,
|
||||||
|
unknown
|
||||||
|
> {
|
||||||
|
constructor(basePath: string) {
|
||||||
|
super(basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHppPerKandangReport(
|
||||||
|
area_id?: string,
|
||||||
|
location_id?: string,
|
||||||
|
kandang_id?: string,
|
||||||
|
weight_min?: string,
|
||||||
|
weight_max?: string,
|
||||||
|
period?: string,
|
||||||
|
sort_by?: string,
|
||||||
|
show_unrecorded?: boolean,
|
||||||
|
page?: number,
|
||||||
|
limit?: number
|
||||||
|
): Promise<BaseApiResponse<HppPerKandangReport> | undefined> {
|
||||||
|
return await this.customRequest<BaseApiResponse<HppPerKandangReport>>(
|
||||||
|
`hpp-per-kandang`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
area_id: area_id,
|
||||||
|
location_id: location_id,
|
||||||
|
kandang_id: kandang_id,
|
||||||
|
weight_min: weight_min,
|
||||||
|
weight_max: weight_max,
|
||||||
|
period: period,
|
||||||
|
sort_by: sort_by,
|
||||||
|
show_unrecorded: show_unrecorded,
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaleReportApi = new MarketingSaleReportService(
|
||||||
|
'reports/marketings'
|
||||||
|
);
|
||||||
|
|
||||||
|
// export const SaleReportApi = new MarketingSaleReportService(
|
||||||
|
// 'http://localhost:4010/api/reports/marketings'
|
||||||
|
// );
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
import { BaseMetadata } from '@types/api/base-metadata';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
|
||||||
|
export type HppPerKandangRow = {
|
||||||
|
id: number;
|
||||||
|
kandang: Kandang;
|
||||||
|
weight_range: {
|
||||||
|
weight_min: number;
|
||||||
|
weight_max: number;
|
||||||
|
};
|
||||||
|
remaining_chicken_birds: number;
|
||||||
|
remaining_chicken_weight_kg: number;
|
||||||
|
avg_weight_kg: number;
|
||||||
|
egg_production_pieces: number;
|
||||||
|
egg_production_kg: number;
|
||||||
|
egg_hpp_rp_per_kg: number;
|
||||||
|
egg_value_rp: number;
|
||||||
|
feed_suppliers: Supplier[];
|
||||||
|
doc_suppliers: Supplier[];
|
||||||
|
average_doc_price_rp: number;
|
||||||
|
hpp_rp: number;
|
||||||
|
remaining_value_rp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HppPerKandangSummaryTotal = {
|
||||||
|
total_remaining_chicken_birds: number;
|
||||||
|
total_remaining_chicken_weight_kg: number;
|
||||||
|
average_weight_kg: number;
|
||||||
|
total_remaining_value_rp: number;
|
||||||
|
total_egg_production_pieces: number;
|
||||||
|
total_egg_production_kg: number;
|
||||||
|
average_egg_hpp_rp_per_kg: number;
|
||||||
|
total_egg_value_rp: number;
|
||||||
|
total_hpp_rp: number;
|
||||||
|
total_average_doc_price_rp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HppPerKandangPerWeightRange = {
|
||||||
|
id: number;
|
||||||
|
weight_range: {
|
||||||
|
weight_min: number;
|
||||||
|
weight_max: number;
|
||||||
|
};
|
||||||
|
label: string;
|
||||||
|
remaining_chicken_birds: number;
|
||||||
|
remaining_chicken_weight_kg: number;
|
||||||
|
avg_weight_kg: number;
|
||||||
|
egg_production_pieces: number;
|
||||||
|
egg_production_kg: number;
|
||||||
|
egg_hpp_rp_per_kg: number;
|
||||||
|
egg_value_rp: number;
|
||||||
|
feed_suppliers: Supplier[];
|
||||||
|
doc_suppliers: Supplier[];
|
||||||
|
average_doc_price_rp: number;
|
||||||
|
hpp_rp: number;
|
||||||
|
remaining_value_rp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HppPerKandangSummary = {
|
||||||
|
per_weight_range: HppPerKandangPerWeightRange[];
|
||||||
|
total: HppPerKandangSummaryTotal;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HppPerKandangReport = BaseMetadata & {
|
||||||
|
period: string;
|
||||||
|
rows: HppPerKandangRow[];
|
||||||
|
summary: HppPerKandangSummary;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user