mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Compare commits
504 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e1fab9a69 | |||
| ef56f87e45 | |||
| c4827bb810 | |||
| 9abb8b0b58 | |||
| 8d014a8fea | |||
| 3d37fb2ecb | |||
| d60877d391 | |||
| b3b60018bb | |||
| c98a51326f | |||
| 7437e2e584 | |||
| ac6f6ecf78 | |||
| 7f961b2f8b | |||
| a8c02243a4 | |||
| 82dca3b57e | |||
| 94d623d793 | |||
| f76b5b981c | |||
| 8df5af0124 | |||
| 3c175d4586 | |||
| 9350a6bd3e | |||
| 6668c7b1f9 | |||
| ce4f50c92a | |||
| 146192a5b3 | |||
| 27c24e7c82 | |||
| a99a399f09 | |||
| 37d0041a4f | |||
| 3647f1a1ea | |||
| 7b5049165a | |||
| 3839b46edc | |||
| b7f2bca931 | |||
| 802bf77bc5 | |||
| fd7b49ab93 | |||
| 456070491f | |||
| c12beca4d7 | |||
| 910981645b | |||
| 82b5429d02 | |||
| 6c6f739fc0 | |||
| 001dafecb7 | |||
| 4bb3ada779 | |||
| 0b63dcb532 | |||
| 23dd220b2f | |||
| 770c293257 | |||
| 3374ab4779 | |||
| 7a668c0cf9 | |||
| 14151f6f5a | |||
| 0275e66eda | |||
| 9bc5842493 | |||
| 4cad8aba64 | |||
| 7b5af69dd1 | |||
| 2e179b74ba | |||
| fe2a2dfb43 | |||
| 910a36857e | |||
| 58ddd9b991 | |||
| bb9c6ab969 | |||
| ddffdd1b27 | |||
| f097620c4b | |||
| 280d790f0c | |||
| 3a2e74b559 | |||
| b9ef0fa338 | |||
| 6d8cdeffe9 | |||
| 2e36247a1a | |||
| 37cd990b4f | |||
| bdc7ac4d22 | |||
| b6c2f36dd1 | |||
| 10cc4bee72 | |||
| ff6bcf019b | |||
| bb0508d456 | |||
| d6dd5e6709 | |||
| 3c75a7631a | |||
| e3d3e744b0 | |||
| 5767a078d9 | |||
| 67c7e85ba8 | |||
| c5a5582147 | |||
| 46cb8a7d61 | |||
| 0189733dec | |||
| d0c3581f57 | |||
| e7569b7448 | |||
| 69b998a61a | |||
| c50c110005 | |||
| 3775bb6093 | |||
| 3dc64d01db | |||
| 2ed8ecbbb7 | |||
| e5f6ef8a85 | |||
| 7ff0891ad5 | |||
| a9a5098a21 | |||
| 7f9bb8e11d | |||
| bef3f365bb | |||
| a0e8c60082 | |||
| e7f378823c | |||
| ba3cb98e2c | |||
| 7643645643 | |||
| 3b1e7e3b03 | |||
| 725111dc0c | |||
| 073d7eee03 | |||
| cce5a8df43 | |||
| 978067ac6c | |||
| 6255367366 | |||
| af9cb8ec6b | |||
| e0a1922ed4 | |||
| 4b5ad0dcab | |||
| ca62b31aa6 | |||
| 4ec32c51b2 | |||
| cdee616e18 | |||
| 50378a2ee2 | |||
| ab093467c4 | |||
| 79e41d8a6f | |||
| 35001ff422 | |||
| 7026619249 | |||
| 3945142966 | |||
| b19099cea2 | |||
| f65593de25 | |||
| a5f1a6ea75 | |||
| 4e58f20ba3 | |||
| dc41d6ce73 | |||
| 8869c9df2c | |||
| f2b3f2b584 | |||
| 31cea258a7 | |||
| 53d7439300 | |||
| 28a1852de8 | |||
| ff92073d19 | |||
| 6ffc2c2806 | |||
| 9e402e373c | |||
| 2e4c19b714 | |||
| 039c926e2d | |||
| e52ba7b394 | |||
| 90dc7c80f2 | |||
| 15c883ca73 | |||
| 47f74b8842 | |||
| ef9009b304 | |||
| ce25758a17 | |||
| 371b236e25 | |||
| a54dd1fa9e | |||
| 31205a44f9 | |||
| 3c9c55e049 | |||
| 7a4f93cf0c | |||
| a738d58c37 | |||
| 46daed8fc4 | |||
| 29347c24f4 | |||
| a0f603b707 | |||
| 631e3959cd | |||
| 2a340a26f9 | |||
| e75246ff8d | |||
| 64aee33452 | |||
| 1851f0e12f | |||
| 8b3f44708d | |||
| a5fd97a175 | |||
| 2ee5d1f7bd | |||
| 6eb257705f | |||
| 9ea1d06972 | |||
| ff8833b5b3 | |||
| 2dd98fd7e3 | |||
| 76fff98d9d | |||
| 18eeabd353 | |||
| 06b5a97de3 | |||
| 5cccc0b3c6 | |||
| 7ab9518a55 | |||
| ac51229398 | |||
| 5a2532a0fa | |||
| f9d2a875e2 | |||
| 6cf8e463c6 | |||
| 4206408db1 | |||
| ff2ed8757f | |||
| 0c5ee08f90 | |||
| bbf9581d3a | |||
| 5830ab4c67 | |||
| a1a0b71814 | |||
| 2b3b6b9549 | |||
| be3034a94e | |||
| a11d05e720 | |||
| cb454e7eb7 | |||
| a6d6c53069 | |||
| c875ebd951 | |||
| a369386922 | |||
| b3198a44e9 | |||
| b2dfb8fec6 | |||
| d4d77bb13a | |||
| 7dfa5233f3 | |||
| 3d910f78db | |||
| 2dfac0be72 | |||
| afe0d2161d | |||
| 68c13c48c7 | |||
| b9a1e94a29 | |||
| d8c6a90c55 | |||
| 4d01ad7d1d | |||
| c487e7f53e | |||
| a316120a78 | |||
| 9af0537587 | |||
| f668bcecb8 | |||
| a12b09eb5f | |||
| cfb96c45c9 | |||
| 747b0f9c2c | |||
| ee2f530d81 | |||
| 617124efe4 | |||
| c0337f4d67 | |||
| e5dcca3408 | |||
| f2b05856bb | |||
| 5d6aaace86 | |||
| 9dcb3d7269 | |||
| e96bb46cfd | |||
| 37edc957d2 | |||
| 60df577cc6 | |||
| e0e2b0c406 | |||
| 244be32b59 | |||
| 1080a26f93 | |||
| c12bf92723 | |||
| 5c5b49d0a9 | |||
| b7f886b51e | |||
| 587266e23d | |||
| 9293b6321f | |||
| ddfd1206a7 | |||
| 75910960c5 | |||
| aae633edee | |||
| f129329d52 | |||
| 2afcc5d1c9 | |||
| 7f578c5d03 | |||
| 180b129550 | |||
| 8b2277c8c3 | |||
| 68f4562395 | |||
| c374a4a4e9 | |||
| bda66381b8 | |||
| 28adeee7bd | |||
| 727ac8ccdb | |||
| b77a8ef56f | |||
| 50e0ccd9e4 | |||
| e43a25307f | |||
| db4750217e | |||
| 19793cdcd4 | |||
| 15bddc43e2 | |||
| 3b7c7bb13f | |||
| 633cece581 | |||
| e455d203cc | |||
| d2a5229282 | |||
| 2391d6ceeb | |||
| 4bb57ed0a0 | |||
| 5bf3d32636 | |||
| c5a0cfe118 | |||
| 898bbd57ec | |||
| 5b5113de6e | |||
| 267a6f37cc | |||
| 7d4898c266 | |||
| f49822d03d | |||
| aa4da686c6 | |||
| 5a668c469f | |||
| 2ca733de97 | |||
| 8afc1a6381 | |||
| d47142153e | |||
| 188385b638 | |||
| 08aa79a06b | |||
| 16741aaa46 | |||
| 93083c7d2a | |||
| 8333b5138a | |||
| c0ee2013f3 | |||
| 022656cd80 | |||
| 9d3c22fcf3 | |||
| 11353809f0 | |||
| 6463b7a572 | |||
| 7a5ee2aca1 | |||
| 5e907d7e53 | |||
| 71edc9c68a | |||
| 2a33fdbbbe | |||
| 178c659b58 | |||
| c1d6436583 | |||
| 8dc62453bd | |||
| 244d800874 | |||
| 52dd1613bb | |||
| 57ea81fdf2 | |||
| 90742604cb | |||
| 4b8853b766 | |||
| 7168270527 | |||
| 47b186e195 | |||
| ff39514b78 | |||
| f97b6fc218 | |||
| 62dc8235d4 | |||
| 73d05d6b4b | |||
| 3c44906a20 | |||
| e4b1deecdc | |||
| 12afa88f2c | |||
| 0527155bb9 | |||
| 34a45d084b | |||
| 9de897dfbd | |||
| 907f6664e1 | |||
| 3ad04e5bac | |||
| 4649dfde89 | |||
| b580a01bdc | |||
| 15ec6c3b9c | |||
| 8b970aeb64 | |||
| de6fd2367e | |||
| 3fedbc7ffb | |||
| 9e297cc0a4 | |||
| 6ff3a715e0 | |||
| cd4cef883e | |||
| d853781c17 | |||
| 8faed2e561 | |||
| 8aeef46ee3 | |||
| c9000c1e2c | |||
| bb3541090a | |||
| ad0e617ed0 | |||
| 73ef1c2ece | |||
| 9cf0d15c33 | |||
| c66f7b1cbf | |||
| 17ebc31f00 | |||
| ce0b4d744c | |||
| f6f3290743 | |||
| 31a4dec8a3 | |||
| fbb6f87368 | |||
| d88d71fb16 | |||
| 570024c2e6 | |||
| 70556d04ba | |||
| 00434002a7 | |||
| bcb1e0b5b6 | |||
| 03a6aabf1f | |||
| 47adaa4f92 | |||
| f2cdbd497a | |||
| 4ffea739a9 | |||
| bf5591d61d | |||
| a725ae4891 | |||
| f5d3fb3b9d | |||
| 4a6c443003 | |||
| 09f4af3ece | |||
| 62d250109b | |||
| e50f4dbddb | |||
| c898154b48 | |||
| acb02c9bdc | |||
| 986f429ea9 | |||
| 1dafb0d365 | |||
| 095b1c5850 | |||
| 4297502c55 | |||
| 726065da51 | |||
| 6c03e42006 | |||
| 5c39e900f3 | |||
| 68c1655824 | |||
| 0ef8c06e41 | |||
| 68b25332b1 | |||
| b402a06706 | |||
| 7df2fad959 | |||
| 0b52fff5f5 | |||
| e251ab9eb4 | |||
| 9f0fbcf041 | |||
| 05fbae680f | |||
| 444c475cb4 | |||
| ef1ce2c78c | |||
| 429ff58bfd | |||
| 8961004000 | |||
| 2dc3bcf9f0 | |||
| 9c31705865 | |||
| b89730ab68 | |||
| 6e34eede4b | |||
| ffd5e70947 | |||
| 2f89c6f216 | |||
| ebf966228b | |||
| ebe1d77c72 | |||
| 922a93414f | |||
| 854a1e7c4c | |||
| 129a3fda44 | |||
| 981b48acc0 | |||
| dd5bbf0ac6 | |||
| 8872b283ac | |||
| 860c9dec22 | |||
| 0cd6c9bd2f | |||
| 107d412c10 | |||
| c6d8533190 | |||
| ae90d55f81 | |||
| c155717459 | |||
| d74de4b2d9 | |||
| 529ba21f47 | |||
| 449c2030fe | |||
| 679740972f | |||
| 57b8326fdf | |||
| e26b5127c5 | |||
| 1a4a1e8e56 | |||
| 10d1f05aa5 | |||
| e4b6238771 | |||
| fc89922ed1 | |||
| 25b5165249 | |||
| cae19d905b | |||
| c040c0e9bb | |||
| b3bd7563fa | |||
| edf21fbfc4 | |||
| 65f31f8340 | |||
| 772087bacd | |||
| f302bcdb4b | |||
| b68bcc77f5 | |||
| 3d7a2073b0 | |||
| 8b1546a305 | |||
| fa3d0a1179 | |||
| ffcf422cb5 | |||
| 8d92da75cf | |||
| 6b29406307 | |||
| 4f3e304b2b | |||
| 18aff48dc2 | |||
| 7bee13124d | |||
| f5d9dcbdf6 | |||
| 04d22e55db | |||
| 8bf5a88edb | |||
| ee0c47b0a1 | |||
| 93d4ff9339 | |||
| aa52211b0a | |||
| 39cb38a23f | |||
| c1087b37fb | |||
| c4e27edd56 | |||
| b020f2b187 | |||
| 375de4c86c | |||
| 8f361c4327 | |||
| 710be88794 | |||
| 0f7a2bd796 | |||
| 0c8a833e00 | |||
| 0c42bfd70c | |||
| 85dee607e0 | |||
| 6c7e310e67 | |||
| 85f6677c2a | |||
| 811850857d | |||
| 5494cb0ff2 | |||
| 11eeac3289 | |||
| 1621f2ab7d | |||
| 5540787154 | |||
| 1b499bc967 | |||
| 44a5c51023 | |||
| aa13e989c1 | |||
| ebe7c367e7 | |||
| 2f085c287f | |||
| 058f9f403d | |||
| 8b8b7be4b7 | |||
| efcecf4f66 | |||
| a6c63a7dcb | |||
| 0263db9fae | |||
| cc08e3af15 | |||
| 0929461ec5 | |||
| ace6633f79 | |||
| f1a952ca6b | |||
| ed34a99117 | |||
| 4beaba1f15 | |||
| 8ea029efdd | |||
| 02e4dba288 | |||
| c42fdbf33d | |||
| 2cfa8c046b | |||
| 30d5516161 | |||
| f83abc91da | |||
| 918c51e83b | |||
| f1a4d9b648 | |||
| 29e33560f8 | |||
| fb9e863862 | |||
| 1b3e5f94f1 | |||
| e1856926ea | |||
| 2b096099d3 | |||
| ea25417e8d | |||
| 668abeca23 | |||
| deabb1c3ee | |||
| 121c44070c | |||
| 0dbad23cd5 | |||
| b9a17f472b | |||
| c07b245eeb | |||
| d7e32f8f5b | |||
| 698fe2e851 | |||
| cdf0442a2b | |||
| 422c7c9fb0 | |||
| 53e018aece | |||
| ca58e19a48 | |||
| 0971e6ddeb | |||
| 25fbf95062 | |||
| b2f6c6c485 | |||
| cc86151631 | |||
| 755f3fa0bb | |||
| ce1114d724 | |||
| 128b765045 | |||
| 92c07e7841 | |||
| 1aba297920 | |||
| 2aef6522bb | |||
| 3bab96c325 | |||
| 847772616e | |||
| 344140e973 | |||
| 3ce1299091 | |||
| aea35d4b9f | |||
| 5b134148a5 | |||
| 32f4cf411f | |||
| 04d01970aa | |||
| 84cbbaf238 | |||
| 9176373072 | |||
| 5c50e4a0c1 | |||
| 7e64ec0f79 | |||
| e2be39af18 | |||
| 9322d6298c | |||
| e9cd84e89e | |||
| 89cfd31155 | |||
| ec5962bccc | |||
| 0eb4fa99a7 | |||
| 2ef8b2dc9f | |||
| aed1a1ed01 | |||
| 2c9c2660c0 | |||
| b840f42ae0 | |||
| 6bc86af32f | |||
| 1603ae62e0 | |||
| 8fd442621a | |||
| 35471fc597 | |||
| bd4242c4fd | |||
| 56bde974ad | |||
| 38258e4311 | |||
| 149e525ff4 | |||
| 8fb761f02c | |||
| 3bc5a5b75e | |||
| 79112e0da8 | |||
| bf9eb91ea2 | |||
| e8c8ffadfe | |||
| 2ae1c5b382 | |||
| 961f81411b | |||
| de439275e0 |
@@ -45,3 +45,9 @@ next-env.d.ts
|
|||||||
|
|
||||||
# claude
|
# claude
|
||||||
.claude
|
.claude
|
||||||
|
|
||||||
|
# rtk
|
||||||
|
rtk.exe
|
||||||
|
|
||||||
|
# local specs
|
||||||
|
/local-specs
|
||||||
+50
-3
@@ -15,7 +15,7 @@ default:
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
.build_template: &build_template
|
.build_template: &build_template
|
||||||
stage: build
|
stage: build
|
||||||
image: node:20-alpine
|
image: public.ecr.aws/docker/library/node:20-alpine
|
||||||
cache:
|
cache:
|
||||||
key: npm-cache
|
key: npm-cache
|
||||||
paths:
|
paths:
|
||||||
@@ -30,6 +30,10 @@ default:
|
|||||||
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
||||||
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
||||||
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
||||||
|
- echo "NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV"
|
||||||
|
- echo "NEXT_PUBLIC_HELPDESK_URL=$NEXT_PUBLIC_HELPDESK_URL"
|
||||||
|
- echo "NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL=$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
|
||||||
|
- echo "NEXT_PUBLIC_S3_PUBLIC_BASE_URL=$NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
|
||||||
- echo "Building Next.js static export..."
|
- echo "Building Next.js static export..."
|
||||||
- npx next build
|
- npx next build
|
||||||
- |
|
- |
|
||||||
@@ -41,7 +45,11 @@ default:
|
|||||||
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||||
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
||||||
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
||||||
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL"
|
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL",
|
||||||
|
"NEXT_PUBLIC_APP_ENV": "$NEXT_PUBLIC_APP_ENV",
|
||||||
|
"NEXT_PUBLIC_HELPDESK_URL": "$NEXT_PUBLIC_HELPDESK_URL",
|
||||||
|
"NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL": "$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
|
||||||
|
"NEXT_PUBLIC_S3_PUBLIC_BASE_URL": "NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -56,7 +64,7 @@ default:
|
|||||||
.deploy_template: &deploy_template
|
.deploy_template: &deploy_template
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image:
|
image:
|
||||||
name: amazon/aws-cli:latest
|
name: public.ecr.aws/aws-cli/aws-cli:latest
|
||||||
entrypoint: ['/bin/sh', '-c']
|
entrypoint: ['/bin/sh', '-c']
|
||||||
script:
|
script:
|
||||||
- set -e
|
- set -e
|
||||||
@@ -142,6 +150,10 @@ build:dev:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
|
NEXT_PUBLIC_APP_ENV: 'development'
|
||||||
|
NEXT_PUBLIC_HELPDESK_URL: 'https://dev-helpdesk.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dev-dashboard-ho.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com'
|
||||||
|
|
||||||
deploy:dev:
|
deploy:dev:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
@@ -170,6 +182,9 @@ build:staging:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
|
NEXT_PUBLIC_APP_ENV: 'staging'
|
||||||
|
NEXT_PUBLIC_HELPDESK_URL: 'https://stg-helpdesk.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://stg-dashboard-ho.mbugroup.id/'
|
||||||
|
|
||||||
deploy:staging:
|
deploy:staging:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
@@ -183,3 +198,35 @@ deploy:staging:
|
|||||||
environment:
|
environment:
|
||||||
name: staging
|
name: staging
|
||||||
url: https://stg-lti-erp.mbugroup.id
|
url: https://stg-lti-erp.mbugroup.id
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# ====== (Branch production) ======
|
||||||
|
# ==========================================================
|
||||||
|
build:production:
|
||||||
|
<<: *build_template
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||||
|
environment:
|
||||||
|
name: staging
|
||||||
|
variables:
|
||||||
|
NEXT_PUBLIC_LTI_URL: 'https://lti-erp.mbugroup.id'
|
||||||
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
||||||
|
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
||||||
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
|
NEXT_PUBLIC_APP_ENV: 'production'
|
||||||
|
NEXT_PUBLIC_HELPDESK_URL: 'https://helpdesk.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dashboard-ho.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com/'
|
||||||
|
|
||||||
|
deploy:production:
|
||||||
|
<<: *deploy_template
|
||||||
|
needs: ['build:production']
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||||
|
variables:
|
||||||
|
S3_BUCKET: 'production-lti-erp.mbugroup.id'
|
||||||
|
AWS_REGION: 'ap-southeast-3'
|
||||||
|
CLOUDFRONT_DISTRIBUTION_ID: 'E1SSLXKYYITASJ'
|
||||||
|
environment:
|
||||||
|
name: staging
|
||||||
|
url: https://lti-erp.mbugroup.id
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npx tsc --noEmit
|
npm run typecheck
|
||||||
|
git add .
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Project-local RTK filters — commit this file with your repo.
|
||||||
|
# Filters here override user-global and built-in filters.
|
||||||
|
# Docs: https://github.com/rtk-ai/rtk#custom-filters
|
||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
# Example: suppress build noise from a custom tool
|
||||||
|
# [filters.my-tool]
|
||||||
|
# description = "Compact my-tool output"
|
||||||
|
# match_command = "^my-tool\\s+build"
|
||||||
|
# strip_ansi = true
|
||||||
|
# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"]
|
||||||
|
# max_lines = 30
|
||||||
|
# on_empty = "my-tool: ok"
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
# LTI Web Client
|
||||||
|
|
||||||
|
Next.js 15 (App Router) + React 19 + TypeScript front-end for the LTI ERP system.
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
- **Framework:** Next.js 15.5 (App Router, Turbopack)
|
||||||
|
- **UI:** React 19, Tailwind CSS v4, Radix UI, daisyUI, lucide-react
|
||||||
|
- **State:** zustand
|
||||||
|
- **Forms:** Formik + Yup, react-hook-form
|
||||||
|
- **Data fetching:** axios + SWR (custom `httpClient` / `httpClientFetcher` in `src/services/http`)
|
||||||
|
- **Tables:** @tanstack/react-table
|
||||||
|
- **Reporting:** @react-pdf/renderer, jspdf, exceljs, xlsx, recharts
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
- `npm run dev` — lint + dev server (Turbopack)
|
||||||
|
- `npm run build` — production build
|
||||||
|
- `npm run lint` — ESLint
|
||||||
|
- `npm run typecheck` — `next typegen && tsc --noEmit`
|
||||||
|
- `npm run format` — Prettier
|
||||||
|
- `npm run pre-commit` — format + lint + typecheck + build (Husky pre-commit hook)
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
app/ # Next.js App Router routes (one folder per feature)
|
||||||
|
components/
|
||||||
|
pages/{feature}/ # Page-specific components (mirrors src/app)
|
||||||
|
helper/ # Cross-cutting helpers (e.g. SuspenseHelper)
|
||||||
|
ui/ # Shared UI primitives
|
||||||
|
services/
|
||||||
|
api/ # API service classes (extend BaseApiService)
|
||||||
|
http/ # httpClient / httpClientFetcher
|
||||||
|
hooks/ # Service-level hooks
|
||||||
|
stores/ # zustand stores grouped by domain
|
||||||
|
types/api/ # Request/response types per feature
|
||||||
|
lib/ # Shared helpers (api-helper, formik-helper, utils, validation, …)
|
||||||
|
config/, styles/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature development standard
|
||||||
|
|
||||||
|
**Always follow this order when adding a new feature.** This is a team convention — deviating creates churn in code review.
|
||||||
|
|
||||||
|
1. **Types** — Define payload and response types in `src/types/api/{feature}` (or `{feature}.d.ts` for small features).
|
||||||
|
2. **API service** — Add `src/services/api/{feature}.ts` exporting a class that extends `BaseApiService<T, CreatePayload, UpdatePayload>` from [src/services/api/base.ts](src/services/api/base.ts). Use a subfolder (e.g. `src/services/api/daily-checklist/`) when the feature has multiple resource classes.
|
||||||
|
3. **Page** — Create the route under `src/app/{feature}` and a matching `src/components/pages/{feature}` folder for its components.
|
||||||
|
4. **Component slicing** — Break the page UI into components inside `src/components/pages/{feature}`.
|
||||||
|
5. **Wire up the API** — Consume the service class from step 2 inside the page/components (often via SWR).
|
||||||
|
6. **Detail layout** — When a route reads URL params via `useSearchParams` (e.g. `/feature/detail?id=123`), add `src/app/{feature}/detail/layout.tsx` that wraps `children` in `<SuspenseHelper>` from `@/components/helper/SuspenseHelper`.
|
||||||
|
7. **Shared state** — Use zustand stores in `src/stores/{domain}` when state must cross component boundaries.
|
||||||
|
8. **Helpers** — Reuse from [src/lib](src/lib) first (`api-helper.ts`, `formik-helper.ts`, `utils/`, `validation/`, etc.). Add new helpers there.
|
||||||
|
|
||||||
|
### Reference implementations
|
||||||
|
|
||||||
|
`closing`, `finance`, `expense`, `production`, `inventory`, `marketing`, `master-data`, `purchase`, `report`, `daily-checklist`, `dashboard` — all live in both `src/app/{feature}` and `src/components/pages/{feature}` and follow the standard above.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Path alias `@/` maps to `src/`.
|
||||||
|
- Detail pages that read `useSearchParams` MUST be wrapped in `<SuspenseHelper>` via a `layout.tsx` (see [src/app/finance/detail/layout.tsx](src/app/finance/detail/layout.tsx) for the canonical pattern).
|
||||||
|
- API service classes inherit CRUD methods (`getAll`, `getSingle`, etc.) from `BaseApiService` — extend the class for feature-specific endpoints rather than calling `httpClient` directly from components.
|
||||||
|
- Pre-commit runs format + lint + typecheck + build; do not bypass with `--no-verify`.
|
||||||
|
|
||||||
|
## Table filter persistence pattern
|
||||||
|
|
||||||
|
Data tables across all modules (master-data, inventory, finance, purchase, etc.) use `useTableFilter` with `persist: true` to persist filter state in localStorage. This allows users' search, pagination, and filter choices to survive page refreshes.
|
||||||
|
|
||||||
|
**Three core principles (apply to all table components):**
|
||||||
|
|
||||||
|
1. **Set formik initialValues from tableFilterState** (not hardcoded defaults)
|
||||||
|
- Ensures the filter modal displays currently active filters when opened
|
||||||
|
- Initialize directly from persisted state: `location: tableFilterState.locationFilter`
|
||||||
|
|
||||||
|
2. **Pass `true` as last parameter to updateFilter calls**
|
||||||
|
- `updateFilter('fieldName', value, true)` immediately persists to localStorage
|
||||||
|
- Resets pagination to page 1 when filters change (via SWR revalidation)
|
||||||
|
- Apply to: search handlers, filter form submissions, reset handlers
|
||||||
|
|
||||||
|
3. **Create custom formikResetHandler function**
|
||||||
|
- Call `resetFilter()` (single call — resets all `useTableFilter` state to defaults)
|
||||||
|
- Reset any local error state (e.g. `setHasDateError(false)`, dismiss toasts)
|
||||||
|
- Call `formik.resetForm({ values: { ...defaults } })` to sync formik to defaults
|
||||||
|
- Call `filterModal.closeModal()` at the end
|
||||||
|
- Attach to form `onReset` handler (not `formik.handleReset`)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
resetFilter();
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); }
|
||||||
|
formik.resetForm({ values: { start_date: '', end_date: '', customers: [], filterBy: undefined } });
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
// ...
|
||||||
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization: Avoid useCallback and useMemo for trivial operations**
|
||||||
|
|
||||||
|
- `useCallback` and `useMemo` add overhead; only use them when the computation is expensive or the result is passed to a memoized child
|
||||||
|
- Simple derivations and pass-through handlers don't need them:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ Good: plain derivation
|
||||||
|
const data = isResponseSuccess(response) ? (response.data ?? []) : [];
|
||||||
|
const meta =
|
||||||
|
isResponseSuccess(response) && response.meta ? response.meta : null;
|
||||||
|
|
||||||
|
// ❌ Avoid: useMemo for trivial conditional access
|
||||||
|
const data = useMemo(
|
||||||
|
() => (isResponseSuccess(response) ? (response.data ?? []) : []),
|
||||||
|
[response]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Good: simple handler
|
||||||
|
const handleChange = (val) => setFieldValue('location', val);
|
||||||
|
|
||||||
|
// ❌ Avoid: unnecessary useCallback
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(val) => setFieldValue('location', val),
|
||||||
|
[setFieldValue]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `useMemo` IS justified for large column definition arrays (TanStack Table re-processes on every render)
|
||||||
|
|
||||||
|
**Best practice: Store OptionType objects directly, not IDs**
|
||||||
|
|
||||||
|
For select inputs, store the complete `OptionType` object (or `OptionType[]` for multi-select) in both formik state and tableFilterState. `useTableFilter`'s `serializeValue` handles serialization automatically:
|
||||||
|
|
||||||
|
- `OptionType<T>` → serialized as `String(value)` in the query string
|
||||||
|
- `OptionType<T>[]` → serialized as comma-separated values (CSV) — ideal for multi-select API params like `customer_ids`, `sales_ids`
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { state: tableFilterState, updateFilter, ... } = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
customers: OptionType<number>[]; // multi-select → serializes as CSV
|
||||||
|
location?: OptionType<string>; // single-select → serializes as value string
|
||||||
|
filterBy?: OptionType<string>; // single-select radio
|
||||||
|
}>({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
customers: [],
|
||||||
|
location: undefined,
|
||||||
|
filterBy: undefined,
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
customers: 'customer_ids', // serializes OptionType[] → "1,2,3"
|
||||||
|
location: 'location_id', // serializes OptionType → "abc"
|
||||||
|
filterBy: 'filter_by',
|
||||||
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'my-table',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize formik directly from tableFilterState (no hardcoded defaults)
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues: {
|
||||||
|
customers: tableFilterState.customers,
|
||||||
|
location: tableFilterState.location,
|
||||||
|
filterBy: tableFilterState.filterBy,
|
||||||
|
},
|
||||||
|
...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use formik values directly — no computed helpers needed
|
||||||
|
<SelectInputCheckbox value={formik.values.customers} onChange={(val) => formik.setFieldValue('customers', Array.isArray(val) ? val : [])} />
|
||||||
|
<SelectInput value={formik.values.location} onChange={(val) => formik.setFieldValue('location', val)} />
|
||||||
|
<SelectInputRadio value={formik.values.filterBy ?? null} onChange={(val) => formik.setFieldValue('filterBy', !Array.isArray(val) ? (val ?? undefined) : undefined)} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**Filter field naming convention**
|
||||||
|
|
||||||
|
- Multi-select fields: use plural entity name — `customers`, `salesPersons`, `locations`
|
||||||
|
- Single-select fields: use descriptive camelCase — `filterBy`, `status`, `category`
|
||||||
|
- No `Filter` suffix (e.g. avoid `customerFilter`, `locationFilter`)
|
||||||
|
|
||||||
|
**Filter modal: pass `openModal` directly, never use `enableReinitialize`**
|
||||||
|
|
||||||
|
`enableReinitialize: true` resets formik mid-interaction whenever `tableFilterState` changes, breaking the modal UX. Pass `filterModal.openModal` directly to the button — no ref wrapper needed. Formik retains its last state across open/close, which is acceptable UX (values sync with `tableFilterState` on submit and reset anyway).
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ Avoid: enableReinitialize breaks modal mid-interaction
|
||||||
|
const formik = useFormik({ initialValues: { ... }, enableReinitialize: true });
|
||||||
|
|
||||||
|
// ❌ Avoid: unnecessary ref indirection
|
||||||
|
const handleFilterModalOpenRef = useRef(() => {});
|
||||||
|
handleFilterModalOpenRef.current = () => { formik.setValues({...}); filterModal.openModal(); };
|
||||||
|
|
||||||
|
// ✅ Correct: pass openModal directly
|
||||||
|
<ButtonFilter onClick={filterModal.openModal} ... />
|
||||||
|
```
|
||||||
|
|
||||||
|
Include `filterModal.openModal` in the `useEffect` deps array when it's used inside the effect.
|
||||||
|
|
||||||
|
**Apply this pattern to:**
|
||||||
|
|
||||||
|
- Any data table component across any module that needs persistent filters
|
||||||
|
- Master-data tables, inventory lists, finance reports, purchase orders, etc.
|
||||||
|
- Whenever users' filter/search/pagination choices should survive page refreshes
|
||||||
|
|
||||||
|
**Reference implementations:**
|
||||||
|
|
||||||
|
- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/`
|
||||||
|
- `BalanceMonitoringTab` in `src/components/pages/report/finance/tab/` — multi-select + radio + date range
|
||||||
|
|
||||||
|
## SWR fetch pattern
|
||||||
|
|
||||||
|
Use `FinanceApi.getAllFetcher` (or the relevant service's `getAllFetcher`) when the result type matches the service generic `T`. When it differs, use `httpClientFetcher` with an explicit type:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ Same type as service generic — use getAllFetcher
|
||||||
|
const { data } = useSWR(
|
||||||
|
`${Api.basePath}${getTableFilterQueryString()}`,
|
||||||
|
Api.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Different type — use httpClientFetcher with explicit useSWR type
|
||||||
|
const { data } = useSWR<
|
||||||
|
BaseApiResponse<BalanceMonitoringRow[]>,
|
||||||
|
AxiosError<BaseApiResponse>,
|
||||||
|
SWRHttpKey
|
||||||
|
>(
|
||||||
|
`${FinanceApi.basePath}/balance-monitoring${getTableFilterQueryString()}`,
|
||||||
|
httpClientFetcher
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Always name the `toQueryString` alias `getTableFilterQueryString` when destructuring from `useTableFilter`.
|
||||||
|
|
||||||
|
## Server-side sorting pattern
|
||||||
|
|
||||||
|
Data tables use TanStack Table's `SortingState` wired to `useTableFilter` so that sorting triggers a server re-fetch rather than client-side reordering.
|
||||||
|
|
||||||
|
**Four-part wiring:**
|
||||||
|
|
||||||
|
1. **Local sort state** — `const [sorting, setSorting] = useState<SortingState>([]);`
|
||||||
|
|
||||||
|
2. **`useTableFilter` config** — Add `sort_by` and `order_by` to `initial` and `paramMap`. The `paramMap` key is the internal name; the value is the query param name sent to the server (they can differ, e.g. `order_by` → `sort_order`):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
initial: { sort_by: '', order_by: '' }
|
||||||
|
paramMap: { sort_by: 'sort_by', order_by: 'sort_order' }
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **`useEffect` sync** — Watches `sorting` and pushes changes into `useTableFilter`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
useEffect(() => {
|
||||||
|
if (sorting.length > 0) {
|
||||||
|
updateFilter('sort_by', sorting[0].id, true);
|
||||||
|
updateFilter('order_by', sorting[0].desc ? 'desc' : 'asc', true);
|
||||||
|
} else {
|
||||||
|
updateFilter('sort_by', '');
|
||||||
|
updateFilter('order_by', '');
|
||||||
|
}
|
||||||
|
}, [sorting]);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **SWR key** — SWR uses `getTableFilterToQueryString()` as its key, so any filter change (including sort) automatically re-fetches with the new query params. TanStack Table's built-in client sorting is effectively disabled; the server does the sorting.
|
||||||
|
|
||||||
|
**Pass `sorting`, `setSorting`, and `manualSorting` to `<Table>`:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Table sorting={sorting} setSorting={handleSortingChange} manualSorting={true} ... />
|
||||||
|
```
|
||||||
|
|
||||||
|
`manualSorting={true}` is required — without it TanStack Table still applies its own client-side sort pass on top of the server-sorted data, producing incorrect order.
|
||||||
|
|
||||||
|
**Reference implementation:** `MarketingTable` in [src/components/pages/marketing/MarketingTable.tsx](src/components/pages/marketing/MarketingTable.tsx).
|
||||||
|
|
||||||
|
## Server-side file export pattern
|
||||||
|
|
||||||
|
All file exports (Excel, PDF, or any format) must use **server-side generation** — the server returns a binary blob and the browser triggers a download. Never generate files client-side with `xlsx`, `@react-pdf/renderer`, `jspdf`, or similar libraries.
|
||||||
|
|
||||||
|
**Rule:** Export methods live in the API service class, not in components. Components only build the query string and call the service method.
|
||||||
|
|
||||||
|
### Service method (in `src/services/api/{feature}.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async exportToExcel(initialQueryString: string) {
|
||||||
|
const params = new URLSearchParams(initialQueryString);
|
||||||
|
|
||||||
|
params.set('export', 'excel'); // or 'pdf', 'csv', etc.
|
||||||
|
params.set('page', '1');
|
||||||
|
params.set('limit', '99999999999');
|
||||||
|
|
||||||
|
const res = await httpClient<Blob>(`${this.basePath}?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([res]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', `filename-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Change `export=excel` → `export=pdf` (and the file extension) for PDF exports.
|
||||||
|
- Add one method per format; keep them side-by-side in the same service class.
|
||||||
|
|
||||||
|
### Component handler (in the page/tab component)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleExportExcel = useCallback(async () => {
|
||||||
|
setIsExcelExportLoading(true);
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filterParams.foo) params.set('foo', filterParams.foo);
|
||||||
|
// ... map all active filter params ...
|
||||||
|
|
||||||
|
await FeatureApi.exportToExcel(params.toString());
|
||||||
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
|
} finally {
|
||||||
|
setIsExcelExportLoading(false);
|
||||||
|
}
|
||||||
|
}, [filterParams, searchValue]);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Do **not** fetch all rows into the component to build the file — delegate entirely to the service method.
|
||||||
|
- Do **not** import `xlsx`, `@react-pdf/renderer`, `jspdf`, `exceljs` in page/tab components.
|
||||||
|
|
||||||
|
**Reference implementation:** `MarketingReportApiService.exportDailyMarketingToExcel` / `exportDailyMarketingToPDF` in [src/services/api/report/marketing-report.ts](src/services/api/report/marketing-report.ts), consumed by [src/components/pages/report/marketing/tab/DailyMarketingTab.tsx](src/components/pages/report/marketing/tab/DailyMarketingTab.tsx).
|
||||||
|
|
||||||
|
<!-- rtk-instructions v2 -->
|
||||||
|
|
||||||
|
# RTK (Rust Token Killer) - Token-Optimized Commands
|
||||||
|
|
||||||
|
## Golden Rule
|
||||||
|
|
||||||
|
**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use.
|
||||||
|
|
||||||
|
**Important**: Even in command chains with `&&`, use `rtk`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ Wrong
|
||||||
|
git add . && git commit -m "msg" && git push
|
||||||
|
|
||||||
|
# ✅ Correct
|
||||||
|
rtk git add . && rtk git commit -m "msg" && rtk git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## RTK Commands by Workflow
|
||||||
|
|
||||||
|
### Build & Compile (80-90% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk cargo build # Cargo build output
|
||||||
|
rtk cargo check # Cargo check output
|
||||||
|
rtk cargo clippy # Clippy warnings grouped by file (80%)
|
||||||
|
rtk tsc # TypeScript errors grouped by file/code (83%)
|
||||||
|
rtk lint # ESLint/Biome violations grouped (84%)
|
||||||
|
rtk prettier --check # Files needing format only (70%)
|
||||||
|
rtk next build # Next.js build with route metrics (87%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test (60-99% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk cargo test # Cargo test failures only (90%)
|
||||||
|
rtk go test # Go test failures only (90%)
|
||||||
|
rtk jest # Jest failures only (99.5%)
|
||||||
|
rtk vitest # Vitest failures only (99.5%)
|
||||||
|
rtk playwright test # Playwright failures only (94%)
|
||||||
|
rtk pytest # Python test failures only (90%)
|
||||||
|
rtk rake test # Ruby test failures only (90%)
|
||||||
|
rtk rspec # RSpec test failures only (60%)
|
||||||
|
rtk test <cmd> # Generic test wrapper - failures only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git (59-80% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk git status # Compact status
|
||||||
|
rtk git log # Compact log (works with all git flags)
|
||||||
|
rtk git diff # Compact diff (80%)
|
||||||
|
rtk git show # Compact show (80%)
|
||||||
|
rtk git add # Ultra-compact confirmations (59%)
|
||||||
|
rtk git commit # Ultra-compact confirmations (59%)
|
||||||
|
rtk git push # Ultra-compact confirmations
|
||||||
|
rtk git pull # Ultra-compact confirmations
|
||||||
|
rtk git branch # Compact branch list
|
||||||
|
rtk git fetch # Compact fetch
|
||||||
|
rtk git stash # Compact stash
|
||||||
|
rtk git worktree # Compact worktree
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Git passthrough works for ALL subcommands, even those not explicitly listed.
|
||||||
|
|
||||||
|
### GitHub (26-87% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk gh pr view <num> # Compact PR view (87%)
|
||||||
|
rtk gh pr checks # Compact PR checks (79%)
|
||||||
|
rtk gh run list # Compact workflow runs (82%)
|
||||||
|
rtk gh issue list # Compact issue list (80%)
|
||||||
|
rtk gh api # Compact API responses (26%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/TypeScript Tooling (70-90% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk pnpm list # Compact dependency tree (70%)
|
||||||
|
rtk pnpm outdated # Compact outdated packages (80%)
|
||||||
|
rtk pnpm install # Compact install output (90%)
|
||||||
|
rtk npm run <script> # Compact npm script output
|
||||||
|
rtk npx <cmd> # Compact npx command output
|
||||||
|
rtk prisma # Prisma without ASCII art (88%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files & Search (60-75% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk ls <path> # Tree format, compact (65%)
|
||||||
|
rtk read <file> # Code reading with filtering (60%)
|
||||||
|
rtk grep <pattern> # Search grouped by file (75%)
|
||||||
|
rtk find <pattern> # Find grouped by directory (70%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analysis & Debug (70-90% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk err <cmd> # Filter errors only from any command
|
||||||
|
rtk log <file> # Deduplicated logs with counts
|
||||||
|
rtk json <file> # JSON structure without values
|
||||||
|
rtk deps # Dependency overview
|
||||||
|
rtk env # Environment variables compact
|
||||||
|
rtk summary <cmd> # Smart summary of command output
|
||||||
|
rtk diff # Ultra-compact diffs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Infrastructure (85% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk docker ps # Compact container list
|
||||||
|
rtk docker images # Compact image list
|
||||||
|
rtk docker logs <c> # Deduplicated logs
|
||||||
|
rtk kubectl get # Compact resource list
|
||||||
|
rtk kubectl logs # Deduplicated pod logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network (65-70% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk curl <url> # Compact HTTP responses (70%)
|
||||||
|
rtk wget <url> # Compact download output (65%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Meta Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk gain # View token savings statistics
|
||||||
|
rtk gain --history # View command history with savings
|
||||||
|
rtk discover # Analyze Claude Code sessions for missed RTK usage
|
||||||
|
rtk proxy <cmd> # Run command without filtering (for debugging)
|
||||||
|
rtk init # Add RTK instructions to CLAUDE.md
|
||||||
|
rtk init --global # Add RTK to ~/.claude/CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token Savings Overview
|
||||||
|
|
||||||
|
| Category | Commands | Typical Savings |
|
||||||
|
| ---------------- | ------------------------------ | --------------- |
|
||||||
|
| Tests | vitest, playwright, cargo test | 90-99% |
|
||||||
|
| Build | next, tsc, lint, prettier | 70-87% |
|
||||||
|
| Git | status, log, diff, add, commit | 59-80% |
|
||||||
|
| GitHub | gh pr, gh run, gh issue | 26-87% |
|
||||||
|
| Package Managers | pnpm, npm, npx | 70-90% |
|
||||||
|
| Files | ls, read, grep, find | 60-75% |
|
||||||
|
| Infrastructure | docker, kubectl | 85% |
|
||||||
|
| Network | curl, wget | 65-70% |
|
||||||
|
|
||||||
|
Overall average: **60-90% token reduction** on common development operations.
|
||||||
|
|
||||||
|
<!-- /rtk-instructions -->
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine
|
FROM public.ecr.aws/docker/library/node:20-alpine
|
||||||
|
|
||||||
RUN apk add --no-cache git bash build-base curl
|
RUN apk add --no-cache git bash build-base curl
|
||||||
|
|
||||||
@@ -22,4 +22,4 @@ RUN mkdir -p .next/server/app/_next && \
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
||||||
|
|||||||
+3
-1
@@ -7,8 +7,10 @@
|
|||||||
"build": "next build --turbopack",
|
"build": "next build --turbopack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
|
"typecheck": "next typegen && tsc --noEmit",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write .",
|
||||||
|
"pre-commit": "npm run format && npm run lint && npm run typecheck && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.1",
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
|
||||||
|
|
||||||
|
const MasterKandangPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<MasterKandangContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MasterKandangPage;
|
||||||
@@ -15,8 +15,8 @@ const ExpenseDetailPage = () => {
|
|||||||
const expenseId = searchParams.get('expenseId');
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
expenseId,
|
['expense-detail', expenseId],
|
||||||
(id: number) => ExpenseApi.getSingle(id)
|
([_, id]) => ExpenseApi.getSingle(Number(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!expenseId) {
|
if (!expenseId) {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { SystemConfigContent } from '@/figma-make/components/pages/master-data/system-config/SystemConfigContent';
|
||||||
|
|
||||||
|
const SystemConfigPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<SystemConfigContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemConfigPage;
|
||||||
@@ -11,10 +11,13 @@ const RecordingEdit = () => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const recordingId = searchParams.get('recordingId');
|
const recordingId = searchParams.get('recordingId');
|
||||||
|
const recordingDetailKey = recordingId
|
||||||
|
? ['recording-detail', recordingId]
|
||||||
|
: null;
|
||||||
|
|
||||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
recordingId,
|
recordingDetailKey,
|
||||||
(id: string) => RecordingApi.getSingle(parseInt(id))
|
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!recordingId) {
|
if (!recordingId) {
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ const RecordingDetail = () => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const recordingId = searchParams.get('recordingId');
|
const recordingId = searchParams.get('recordingId');
|
||||||
|
const recordingDetailKey = recordingId
|
||||||
|
? ['recording-detail', recordingId]
|
||||||
|
: null;
|
||||||
|
|
||||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
recordingId,
|
recordingDetailKey,
|
||||||
(id: string) => RecordingApi.getSingle(parseInt(id))
|
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!recordingId) {
|
if (!recordingId) {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -51,7 +51,7 @@ const Button = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!href && (
|
{(!href || (href && disabled)) && (
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
type={type}
|
type={type}
|
||||||
@@ -68,9 +68,9 @@ const Button = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{href && (
|
{href && !disabled && (
|
||||||
<Link
|
<Link
|
||||||
href={disabled ? '#' : href}
|
href={href}
|
||||||
target={target}
|
target={target}
|
||||||
rel={rel}
|
rel={rel}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ const Pagination = ({
|
|||||||
|
|
||||||
const PageInfo = () => (
|
const PageInfo = () => (
|
||||||
<span className='text-nowrap text-sm font-medium text-base-content/50'>
|
<span className='text-nowrap text-sm font-medium text-base-content/50'>
|
||||||
Page {currentPage} of {totalPages}
|
Total Item: {totalItems} | Page {currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ const Table = <TData extends object>({
|
|||||||
const tableOptions: TableOptions<TData> = {
|
const tableOptions: TableOptions<TData> = {
|
||||||
columns,
|
columns,
|
||||||
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
|
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
|
||||||
|
defaultColumn: { sortDescFirst: false },
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
|||||||
+14
-11
@@ -6,6 +6,7 @@ export interface TabItem {
|
|||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
content?: ReactNode;
|
content?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
hide?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsProps
|
export interface TabsProps
|
||||||
@@ -122,17 +123,19 @@ const Tabs = ({
|
|||||||
>
|
>
|
||||||
<div className={getSideContentClasses()}>
|
<div className={getSideContentClasses()}>
|
||||||
<div role='tablist' className={getTabsClasses()}>
|
<div role='tablist' className={getTabsClasses()}>
|
||||||
{tabs.map(({ id, label, disabled }) => (
|
{tabs.map(({ id, label, disabled, hide }) =>
|
||||||
<button
|
hide ? null : (
|
||||||
key={id}
|
<button
|
||||||
role='tab'
|
key={id}
|
||||||
className={getTabClasses(id === activeTabId, disabled)}
|
role='tab'
|
||||||
onClick={() => !disabled && handleTabChange(id)}
|
className={getTabClasses(id === activeTabId, disabled)}
|
||||||
disabled={disabled}
|
onClick={() => !disabled && handleTabChange(id)}
|
||||||
>
|
disabled={disabled}
|
||||||
{label}
|
>
|
||||||
</button>
|
{label}
|
||||||
))}
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{sideContent && sideContent}
|
{sideContent && sideContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ const NumberInput = ({
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (newChangeEvent) {
|
if (newChangeEvent) {
|
||||||
newChangeEvent.target.value = numberFormatValues.value;
|
newChangeEvent.target.value = parseFloat(
|
||||||
|
numberFormatValues.value
|
||||||
|
) as unknown as string;
|
||||||
|
|
||||||
onChange?.(newChangeEvent);
|
onChange?.(newChangeEvent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import {
|
|||||||
} from '@/types/api/api-general';
|
} from '@/types/api/api-general';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
export interface OptionType {
|
export interface OptionType<T = string | number> {
|
||||||
value: string | number;
|
value: T;
|
||||||
label: string;
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
@@ -523,7 +523,7 @@ const useSelect = <T,>(
|
|||||||
|
|
||||||
const qs = new URLSearchParams({
|
const qs = new URLSearchParams({
|
||||||
...(params ?? {}),
|
...(params ?? {}),
|
||||||
[searchKey]: inputValue ?? '',
|
[searchKey ? searchKey : 'search']: inputValue ?? '',
|
||||||
[pageKey]: String(pageIndex + 1),
|
[pageKey]: String(pageIndex + 1),
|
||||||
[limitKey]: String(limit),
|
[limitKey]: String(limit),
|
||||||
}).toString();
|
}).toString();
|
||||||
@@ -566,23 +566,31 @@ const useSelect = <T,>(
|
|||||||
setSize(size + 1);
|
setSize(size + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
|
|
||||||
let formattedErrorRawData: ErrorApiResponse | undefined = undefined;
|
|
||||||
|
|
||||||
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
|
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
|
||||||
|
|
||||||
if (isResponseSuccess(pages?.[latestPagesIndex])) {
|
const { formattedSuccessRawData, formattedErrorRawData } = useMemo(() => {
|
||||||
formattedSuccessRawData = {
|
let successData: SuccessApiResponse<T[]> | undefined = undefined;
|
||||||
...pages?.[latestPagesIndex],
|
let errorData: ErrorApiResponse | undefined = undefined;
|
||||||
data:
|
|
||||||
pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ??
|
|
||||||
[],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isResponseError(pages?.[latestPagesIndex])) {
|
if (isResponseSuccess(pages?.[latestPagesIndex])) {
|
||||||
formattedErrorRawData = pages?.[latestPagesIndex];
|
successData = {
|
||||||
}
|
...pages![latestPagesIndex],
|
||||||
|
data:
|
||||||
|
pages?.flatMap((page) =>
|
||||||
|
isResponseSuccess(page) ? page.data : []
|
||||||
|
) ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResponseError(pages?.[latestPagesIndex])) {
|
||||||
|
errorData = pages![latestPagesIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formattedSuccessRawData: successData,
|
||||||
|
formattedErrorRawData: errorData,
|
||||||
|
};
|
||||||
|
}, [pages, latestPagesIndex]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputValue,
|
inputValue,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
secondaryButton={
|
secondaryButton={
|
||||||
secondaryButton
|
secondaryButton
|
||||||
? {
|
? {
|
||||||
|
...secondaryButton,
|
||||||
text: secondaryButton?.text ?? 'Tidak',
|
text: secondaryButton?.text ?? 'Tidak',
|
||||||
onClick: (e) => {
|
onClick: (e) => {
|
||||||
if (secondaryButton && secondaryButton?.onClick) {
|
if (secondaryButton && secondaryButton?.onClick) {
|
||||||
|
|||||||
@@ -112,12 +112,11 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
kandangData={kandangData}
|
kandangData={kandangData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!kandangData && (
|
<ClosingKandangList
|
||||||
<ClosingKandangList
|
initialValue={initialValue}
|
||||||
initialValue={initialValue}
|
projectData={projectData}
|
||||||
projectData={projectData}
|
selectedKandangId={kandangData?.id}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTabId}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { ProjectFlock } from '@/types/api/production/project-flock';
|
|||||||
const ClosingKandangList = ({
|
const ClosingKandangList = ({
|
||||||
initialValue,
|
initialValue,
|
||||||
projectData,
|
projectData,
|
||||||
|
selectedKandangId,
|
||||||
}: {
|
}: {
|
||||||
initialValue?: ClosingGeneralInformation;
|
initialValue?: ClosingGeneralInformation;
|
||||||
projectData?: ProjectFlock;
|
projectData?: ProjectFlock;
|
||||||
|
selectedKandangId?: number;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
|
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
|
||||||
@@ -22,6 +24,9 @@ const ClosingKandangList = ({
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
|
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
|
||||||
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
||||||
|
disabled={
|
||||||
|
selectedKandangId === kandang.project_flock_kandang_id
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{kandang.name}
|
{kandang.name}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => {
|
|||||||
{
|
{
|
||||||
id: 'kandang',
|
id: 'kandang',
|
||||||
accessorKey: 'kandang',
|
accessorKey: 'kandang',
|
||||||
header: 'Kandang',
|
header: 'Kandang Atribusi',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const kandang = props.getValue() as Kandang;
|
const kandang = props.getValue() as Kandang;
|
||||||
return kandang?.name || '-';
|
return kandang?.name || '-';
|
||||||
|
|||||||
@@ -127,11 +127,11 @@ const ClosingOutgoingSapronaksTable = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'source_warehouse',
|
accessorKey: 'source_warehouse',
|
||||||
header: 'Gudang Asal',
|
header: 'Gudang Asal (Fisik)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'destination_warehouse',
|
accessorKey: 'destination_warehouse',
|
||||||
header: 'Gudang Tujuan',
|
header: 'Gudang Tujuan (Fisik)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'quantity',
|
accessorKey: 'quantity',
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { DashboardApi } from '@/services/api/dashboard';
|
import { DashboardApi } from '@/services/api/dashboard';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import {
|
||||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
ProjectFlockApi,
|
||||||
|
ProjectFlockKandangApi,
|
||||||
|
} from '@/services/api/production';
|
||||||
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF';
|
import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF';
|
||||||
import {
|
import {
|
||||||
DashboardFilterType,
|
DashboardFilterType,
|
||||||
@@ -22,10 +25,7 @@ import DashboardExportCharts, {
|
|||||||
DashboardExportChartsRef,
|
DashboardExportChartsRef,
|
||||||
} from '@/components/pages/dashboard/export/DashboardExportCharts';
|
} from '@/components/pages/dashboard/export/DashboardExportCharts';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
||||||
import {
|
import { DashboardMeta } from '@/types/api/dashboard/dashboard';
|
||||||
DashboardFilter,
|
|
||||||
DashboardMeta,
|
|
||||||
} from '@/types/api/dashboard/dashboard';
|
|
||||||
import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
|
import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
@@ -42,6 +42,8 @@ import { cn } from '@/lib/helper';
|
|||||||
import DashboardExportStats, {
|
import DashboardExportStats, {
|
||||||
DashboardExportStatsRef,
|
DashboardExportStatsRef,
|
||||||
} from '@/components/pages/dashboard/export/DashboardExportStats';
|
} from '@/components/pages/dashboard/export/DashboardExportStats';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
|
|
||||||
// Helper function to normalize values to array
|
// Helper function to normalize values to array
|
||||||
const normalizeToArray = (
|
const normalizeToArray = (
|
||||||
@@ -68,7 +70,6 @@ const DashboardProduction = () => {
|
|||||||
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
||||||
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
|
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
|
||||||
);
|
);
|
||||||
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
|
|
||||||
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>(
|
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>(
|
||||||
normalizeToArray(filterValues.location)
|
normalizeToArray(filterValues.location)
|
||||||
);
|
);
|
||||||
@@ -80,9 +81,29 @@ const DashboardProduction = () => {
|
|||||||
const {
|
const {
|
||||||
data: dashboardProductionResponse,
|
data: dashboardProductionResponse,
|
||||||
isLoading: isLoadingDashboardProductionData,
|
isLoading: isLoadingDashboardProductionData,
|
||||||
mutate: refreshDashboardProductionData,
|
} = useSWR(
|
||||||
} = useSWR(endpointUrl, () =>
|
[
|
||||||
DashboardApi.getDashboardProductionFetcher(endpointUrl)
|
'dashboard-production',
|
||||||
|
filterValues.startDate ?? '',
|
||||||
|
filterValues.endDate ?? '',
|
||||||
|
filterValues.analysisMode ?? 'OVERVIEW',
|
||||||
|
normalizeToArray(filterValues.location).toString(),
|
||||||
|
normalizeToArray(filterValues.flock).toString(),
|
||||||
|
normalizeToArray(filterValues.kandang).toString(),
|
||||||
|
filterValues.comparisonType ?? '',
|
||||||
|
],
|
||||||
|
() =>
|
||||||
|
DashboardApi.getDashboardProductionFetcher({
|
||||||
|
start_date: filterValues.startDate || '',
|
||||||
|
end_date: filterValues.endDate || '',
|
||||||
|
analysis_mode:
|
||||||
|
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') ||
|
||||||
|
'OVERVIEW',
|
||||||
|
location_ids: normalizeToArray(filterValues.location),
|
||||||
|
flock_ids: normalizeToArray(filterValues.flock),
|
||||||
|
kandang_ids: normalizeToArray(filterValues.kandang),
|
||||||
|
comparison_type: filterValues.comparisonType || '',
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const dashboardProductionData = isResponseSuccess(dashboardProductionResponse)
|
const dashboardProductionData = isResponseSuccess(dashboardProductionResponse)
|
||||||
@@ -95,23 +116,23 @@ const DashboardProduction = () => {
|
|||||||
options: flockOptions,
|
options: flockOptions,
|
||||||
isLoadingOptions: isLoadingFlockOptions,
|
isLoadingOptions: isLoadingFlockOptions,
|
||||||
loadMore: loadMoreFlock,
|
loadMore: loadMoreFlock,
|
||||||
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
|
} = useSelect<ProjectFlock>(
|
||||||
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
ProjectFlockApi.basePath,
|
||||||
});
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setInputValueLocation,
|
setInputValue: setInputValueLocation,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
loadMore: loadMoreLocation,
|
loadMore: loadMoreLocation,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name');
|
} = useSelect(LocationApi.basePath, 'id', 'name');
|
||||||
const {
|
|
||||||
setInputValue: setInputValueKandang,
|
|
||||||
options: kandangOptions,
|
|
||||||
isLoadingOptions: isLoadingKandangOptions,
|
|
||||||
loadMore: loadMoreKandang,
|
|
||||||
} = useSelect(KandangApi.basePath, 'id', 'name', '', {
|
|
||||||
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
|
||||||
});
|
|
||||||
const comparisonTypeOptions = [
|
const comparisonTypeOptions = [
|
||||||
{ value: 'FARM', label: 'Farm' },
|
{ value: 'FARM', label: 'Farm' },
|
||||||
{ value: 'FLOCK', label: 'Flock' },
|
{ value: 'FLOCK', label: 'Flock' },
|
||||||
@@ -135,68 +156,43 @@ const DashboardProduction = () => {
|
|||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: getDashboardFilterSchema(analysisMode),
|
validationSchema: getDashboardFilterSchema(analysisMode),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
// Save filter values to store
|
|
||||||
setFilterValues(values);
|
setFilterValues(values);
|
||||||
|
filterModal.closeModal();
|
||||||
handleApplyFilter({
|
|
||||||
start_date: values.startDate || '',
|
|
||||||
end_date: values.endDate || '',
|
|
||||||
analysis_mode: values.analysisMode as 'OVERVIEW' | 'COMPARISON',
|
|
||||||
location_ids: normalizeToArray(values.location),
|
|
||||||
flock_ids: normalizeToArray(values.flock),
|
|
||||||
kandang_ids: normalizeToArray(values.kandang),
|
|
||||||
comparison_type: values.comparisonType,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resetForm } = formik;
|
const { resetForm } = formik;
|
||||||
|
|
||||||
|
const selectedLocationValues = normalizeToArray(formik.values.location);
|
||||||
|
const selectedFlockValues = normalizeToArray(formik.values.flock);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setInputValueKandang,
|
||||||
|
options: kandangOptions,
|
||||||
|
isLoadingOptions: isLoadingKandangOptions,
|
||||||
|
loadMore: loadMoreKandang,
|
||||||
|
} = useSelect<ProjectFlockKandang>(
|
||||||
|
ProjectFlockKandangApi.basePath,
|
||||||
|
'kandang_id',
|
||||||
|
'kandang.name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
location_id:
|
||||||
|
selectedLocationValues.length > 0
|
||||||
|
? selectedLocationValues.toString()
|
||||||
|
: '',
|
||||||
|
project_flock_id:
|
||||||
|
selectedFlockValues.length > 0 ? selectedFlockValues.toString() : '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const handleResetFilter = useCallback(() => {
|
const handleResetFilter = useCallback(() => {
|
||||||
resetForm();
|
resetForm();
|
||||||
resetFilterValues(); // Clear stored filter values
|
resetFilterValues(); // Clear stored filter values
|
||||||
setAnalysisMode('OVERVIEW');
|
setAnalysisMode('OVERVIEW');
|
||||||
setEndpointUrl('/dashboards');
|
|
||||||
setSelectedLocationIds([]);
|
setSelectedLocationIds([]);
|
||||||
}, [resetForm, resetFilterValues]);
|
filterModal.closeModal();
|
||||||
|
}, [filterModal, resetForm, resetFilterValues]);
|
||||||
const handleApplyFilter = useCallback(
|
|
||||||
(values: DashboardFilter) => {
|
|
||||||
// Build query params object, only include non-empty values
|
|
||||||
const params: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (values.start_date) params.start_date = values.start_date;
|
|
||||||
if (values.end_date) params.end_date = values.end_date;
|
|
||||||
if (values.analysis_mode) params.analysis_mode = values.analysis_mode;
|
|
||||||
if (values.location_ids.length > 0)
|
|
||||||
params.location_ids = values.location_ids.toString();
|
|
||||||
if (values.flock_ids.length > 0)
|
|
||||||
params.flock_ids = values.flock_ids.toString();
|
|
||||||
if (values.kandang_ids.length > 0)
|
|
||||||
params.kandang_ids = values.kandang_ids.toString();
|
|
||||||
if (values.comparison_type)
|
|
||||||
params.comparison_type = values.comparison_type;
|
|
||||||
|
|
||||||
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
|
|
||||||
filterModal.closeModal();
|
|
||||||
refreshDashboardProductionData();
|
|
||||||
},
|
|
||||||
[filterModal, refreshDashboardProductionData]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== Load filter from store on mount =====
|
|
||||||
useEffect(() => {
|
|
||||||
if (!filterValues) return;
|
|
||||||
handleApplyFilter({
|
|
||||||
start_date: filterValues.startDate,
|
|
||||||
end_date: filterValues.endDate,
|
|
||||||
analysis_mode: filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON',
|
|
||||||
location_ids: normalizeToArray(filterValues.location),
|
|
||||||
flock_ids: normalizeToArray(filterValues.flock),
|
|
||||||
kandang_ids: normalizeToArray(filterValues.kandang),
|
|
||||||
comparison_type: filterValues.comparisonType,
|
|
||||||
});
|
|
||||||
}, [filterValues, handleApplyFilter]);
|
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||||
@@ -268,14 +264,6 @@ const DashboardProduction = () => {
|
|||||||
};
|
};
|
||||||
}, [clearNavbarActions]);
|
}, [clearNavbarActions]);
|
||||||
|
|
||||||
if (isLoadingDashboardProductionData) {
|
|
||||||
return (
|
|
||||||
<div className='w-full min-h-screen flex items-center justify-center'>
|
|
||||||
<span className='loading loading-spinner loading-xl'></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full p-3 space-y-3'>
|
<section className='w-full p-3 space-y-3'>
|
||||||
@@ -327,9 +315,15 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* Dashboard Stats */}
|
{/* Dashboard Stats */}
|
||||||
<div>
|
<div>
|
||||||
<DashboardStats
|
{isLoadingDashboardProductionData ? (
|
||||||
data={dashboardProductionData?.statistics_data ?? []}
|
<div className='w-full min-h-screen flex items-center justify-center'>
|
||||||
/>
|
<span className='loading loading-spinner loading-xl'></span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DashboardStats
|
||||||
|
data={dashboardProductionData?.statistics_data ?? []}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Use DashboardLineChart component or skeleton */}
|
{/* Use DashboardLineChart component or skeleton */}
|
||||||
@@ -537,6 +531,7 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -573,6 +568,7 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
@@ -604,6 +600,7 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -643,6 +640,7 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
@@ -669,6 +667,7 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -707,6 +706,7 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
@@ -733,6 +733,7 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
@@ -15,6 +16,7 @@ interface ExpenseDetailProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||||
|
const router = useRouter();
|
||||||
const [activeTab, setActiveTab] = useState<string>('request');
|
const [activeTab, setActiveTab] = useState<string>('request');
|
||||||
|
|
||||||
const expenseDetailTabs = useMemo(() => {
|
const expenseDetailTabs = useMemo(() => {
|
||||||
@@ -46,8 +48,8 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
<section className='w-full max-w-full pb-16'>
|
<section className='w-full max-w-full pb-16'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
|
||||||
variant='link'
|
variant='link'
|
||||||
|
onClick={router.back}
|
||||||
className='w-fit p-0 text-primary'
|
className='w-fit p-0 text-primary'
|
||||||
>
|
>
|
||||||
<Icon icon='uil:arrow-left' width={24} height={24} />
|
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -16,6 +19,7 @@ import {
|
|||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { buildExpenseActionHref } from '@/lib/expense-list-navigation';
|
||||||
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
|
|
||||||
interface ExpenseRealizationContentProps {
|
interface ExpenseRealizationContentProps {
|
||||||
@@ -25,6 +29,8 @@ interface ExpenseRealizationContentProps {
|
|||||||
const ExpenseRealizationContent = ({
|
const ExpenseRealizationContent = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
}: ExpenseRealizationContentProps) => {
|
}: ExpenseRealizationContentProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
documents: [],
|
documents: [],
|
||||||
@@ -74,7 +80,11 @@ const ExpenseRealizationContent = ({
|
|||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
href={`/expense/realization/edit/?expenseId=${initialValues?.id}`}
|
href={buildExpenseActionHref(
|
||||||
|
'/expense/realization/edit/',
|
||||||
|
initialValues?.id as number,
|
||||||
|
searchParams
|
||||||
|
)}
|
||||||
className='px-4 grow sm:grow-0'
|
className='px-4 grow sm:grow-0'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { useSWRConfig } from 'swr';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
@@ -26,11 +28,15 @@ import {
|
|||||||
UploadRequestDocumentsFormSchema,
|
UploadRequestDocumentsFormSchema,
|
||||||
UploadRequestDocumentsFormValues,
|
UploadRequestDocumentsFormValues,
|
||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import {
|
||||||
|
buildExpenseActionHref,
|
||||||
|
getExpenseListReturnTo,
|
||||||
|
} from '@/lib/expense-list-navigation';
|
||||||
|
|
||||||
interface ExpenseRequestContentProps {
|
interface ExpenseRequestContentProps {
|
||||||
initialValues?: Expense;
|
initialValues?: Expense;
|
||||||
@@ -40,6 +46,13 @@ const ExpenseRequestContent = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: ExpenseRequestContentProps) => {
|
}: ExpenseRequestContentProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
const refreshExpense = () => {
|
||||||
|
mutate((key) => Array.isArray(key) && key[0] === 'expense-detail');
|
||||||
|
};
|
||||||
|
|
||||||
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
||||||
useApprovalSteps({
|
useApprovalSteps({
|
||||||
@@ -89,17 +102,24 @@ const ExpenseRequestContent = ({
|
|||||||
!isLatestApprovalRejected &&
|
!isLatestApprovalRejected &&
|
||||||
initialValues?.latest_approval.step_number === 4;
|
initialValues?.latest_approval.step_number === 4;
|
||||||
|
|
||||||
|
const isExpensePaidOff = initialValues?.is_paid;
|
||||||
|
|
||||||
|
const showPaidOffButton =
|
||||||
|
!isExpensePaidOff && (initialValues?.latest_approval.step_number ?? 0) >= 4;
|
||||||
|
|
||||||
// Modal hooks
|
// Modal hooks
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const completeModal = useModal();
|
const completeModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const paidOffModal = useModal();
|
||||||
|
|
||||||
// Modal loading state
|
// Modal loading state
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [isPaidOffLoading, setIsPaidOffLoading] = useState(false);
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
@@ -140,7 +160,31 @@ const ExpenseRequestContent = ({
|
|||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const paidOffClickHandler = () => {
|
||||||
|
paidOffModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
// Modal confirm click handler
|
// Modal confirm click handler
|
||||||
|
const confirmationModalPaidOffClickHandler = async () => {
|
||||||
|
setIsPaidOffLoading(true);
|
||||||
|
|
||||||
|
const paidOffResponse = await ExpenseApi.setExpensePaidOff(
|
||||||
|
initialValues?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(paidOffResponse)) {
|
||||||
|
toast.success('Berhasil menandai biaya operasional sebagai lunas!');
|
||||||
|
refreshExpense();
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
'Gagal menandai biaya operasional sebagai lunas!: ' +
|
||||||
|
paidOffResponse?.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
paidOffModal.closeModal();
|
||||||
|
setIsPaidOffLoading(false);
|
||||||
|
};
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
@@ -148,7 +192,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
if (isResponseSuccess(deleteResponse)) {
|
if (isResponseSuccess(deleteResponse)) {
|
||||||
toast.success('Berhasil menghapus data biaya operasional!');
|
toast.success('Berhasil menghapus data biaya operasional!');
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Gagal menghapus data biaya operasional!');
|
toast.error('Gagal menghapus data biaya operasional!');
|
||||||
}
|
}
|
||||||
@@ -164,7 +208,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
if (isResponseSuccess(completeRes)) {
|
if (isResponseSuccess(completeRes)) {
|
||||||
toast.success(completeRes.message);
|
toast.success(completeRes.message);
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
} else {
|
} else {
|
||||||
toast.error(completeRes?.message as string);
|
toast.error(completeRes?.message as string);
|
||||||
}
|
}
|
||||||
@@ -204,7 +248,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
toast.success(approveResponse?.message);
|
toast.success(approveResponse?.message);
|
||||||
setApprovalNotes('');
|
setApprovalNotes('');
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
} else {
|
} else {
|
||||||
approveModal.closeModal();
|
approveModal.closeModal();
|
||||||
|
|
||||||
@@ -239,7 +283,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
toast.success(rejectResponse.message);
|
toast.success(rejectResponse.message);
|
||||||
setApprovalNotes('');
|
setApprovalNotes('');
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
} else {
|
} else {
|
||||||
rejectModal.closeModal();
|
rejectModal.closeModal();
|
||||||
|
|
||||||
@@ -279,8 +323,6 @@ const ExpenseRequestContent = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='w-full mt-4 flex flex-col gap-4'>
|
<div className='w-full mt-4 flex flex-col gap-4'>
|
||||||
{/* TODO: apply RBAC */}
|
|
||||||
|
|
||||||
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||||
{isCurrentApprovalOnHeadArea && (
|
{isCurrentApprovalOnHeadArea && (
|
||||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||||
@@ -367,7 +409,11 @@ const ExpenseRequestContent = ({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='info'
|
color='info'
|
||||||
href={`/expense/realization/?expenseId=${initialValues?.id}`}
|
href={buildExpenseActionHref(
|
||||||
|
'/expense/realization/',
|
||||||
|
initialValues?.id as number,
|
||||||
|
searchParams
|
||||||
|
)}
|
||||||
className='w-full sm:w-fit'
|
className='w-full sm:w-fit'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -380,13 +426,35 @@ const ExpenseRequestContent = ({
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPaidOffButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='success'
|
||||||
|
onClick={paidOffClickHandler}
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-circle-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Tandai Lunas
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
<RequirePermission permissions='lti.expense.update'>
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
|
href={buildExpenseActionHref(
|
||||||
|
'/expense/detail/edit/',
|
||||||
|
initialValues?.id as number,
|
||||||
|
searchParams
|
||||||
|
)}
|
||||||
className='px-4 grow sm:grow-0'
|
className='px-4 grow sm:grow-0'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||||
@@ -521,6 +589,19 @@ const ExpenseRequestContent = ({
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status Lunas</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
<StatusBadge
|
||||||
|
color={initialValues?.is_paid ? 'primary' : 'warning'}
|
||||||
|
text={initialValues?.is_paid ? 'Lunas' : 'Belum Lunas'}
|
||||||
|
className={{
|
||||||
|
badge: 'w-fit whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Dokumen Pengajuan</th>
|
<th>Dokumen Pengajuan</th>
|
||||||
<th>:</th>
|
<th>:</th>
|
||||||
@@ -536,21 +617,15 @@ const ExpenseRequestContent = ({
|
|||||||
<ul className='list-disc'>
|
<ul className='list-disc'>
|
||||||
{initialValues?.documents.map(
|
{initialValues?.documents.map(
|
||||||
(requestDocument, requestDocumentIdx) => {
|
(requestDocument, requestDocumentIdx) => {
|
||||||
const path = requestDocument.path.startsWith(
|
|
||||||
'/'
|
|
||||||
)
|
|
||||||
? requestDocument.path.slice(1)
|
|
||||||
: requestDocument.path;
|
|
||||||
const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
|
|
||||||
return (
|
return (
|
||||||
<li key={requestDocumentIdx}>
|
<li key={requestDocumentIdx}>
|
||||||
<Link
|
<Link
|
||||||
href={documentUrl}
|
href={requestDocument.path}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='text-blue-500 underline'
|
className='text-blue-500 underline'
|
||||||
>
|
>
|
||||||
{requestDocument.path}{' '}
|
{requestDocument.name}{' '}
|
||||||
<Icon
|
<Icon
|
||||||
icon='cuida:open-in-new-tab-outline'
|
icon='cuida:open-in-new-tab-outline'
|
||||||
width={12}
|
width={12}
|
||||||
@@ -746,6 +821,21 @@ const ExpenseRequestContent = ({
|
|||||||
onClick: confirmationModalRejectClickHandler,
|
onClick: confirmationModalRejectClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={paidOffModal.ref}
|
||||||
|
type='success'
|
||||||
|
text='Apakah anda yakin ingin menandai biaya operasional ini sebagai lunas?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isPaidOffLoading,
|
||||||
|
onClick: confirmationModalPaidOffClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,26 +3,60 @@ import * as yup from 'yup';
|
|||||||
export type ExpensesFilterType = {
|
export type ExpensesFilterType = {
|
||||||
transaction_date: string | null;
|
transaction_date: string | null;
|
||||||
realization_date: string | null;
|
realization_date: string | null;
|
||||||
location_id: string | null;
|
location: { value: number; label: string } | null;
|
||||||
vendor_id: string | null;
|
vendor: { value: number; label: string } | null;
|
||||||
|
category: { value: string; label: string } | null;
|
||||||
|
approval_status: { value: string; label: string } | null;
|
||||||
|
realization_status: { value: string; label: string } | null;
|
||||||
|
project_flock: { value: number; label: string } | null;
|
||||||
|
project_flock_kandang: { value: number; label: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpensesFilterSchema = yup.object({
|
export const ExpensesFilterSchema = yup.object({
|
||||||
transaction_date: yup.string().nullable(),
|
transaction_date: yup.string().nullable(),
|
||||||
realization_date: yup
|
realization_date: yup.string().nullable(),
|
||||||
.string()
|
location: yup
|
||||||
.nullable()
|
.object({
|
||||||
.test(
|
value: yup.number().required(),
|
||||||
'is-greater-or-equal-transaction',
|
label: yup.string().required(),
|
||||||
'Tanggal realisasi tidak boleh sebelum tanggal transaksi',
|
})
|
||||||
function (value) {
|
.nullable(),
|
||||||
const { transaction_date } = this.parent;
|
vendor: yup
|
||||||
if (!transaction_date || !value) return true;
|
.object({
|
||||||
return new Date(value) >= new Date(transaction_date);
|
value: yup.number().required(),
|
||||||
}
|
label: yup.string().required(),
|
||||||
),
|
})
|
||||||
location_id: yup.string().nullable(),
|
.nullable(),
|
||||||
vendor_id: yup.string().nullable(),
|
category: yup
|
||||||
|
.object({
|
||||||
|
value: yup.string().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
approval_status: yup
|
||||||
|
.object({
|
||||||
|
value: yup.string().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
realization_status: yup
|
||||||
|
.object({
|
||||||
|
value: yup.string().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
project_flock: yup
|
||||||
|
.object({
|
||||||
|
value: yup.number().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
project_flock_kandang: yup
|
||||||
|
.object({
|
||||||
|
value: yup.number().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject } from 'react';
|
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -11,8 +11,11 @@ import SelectInput from '@/components/input/SelectInput';
|
|||||||
|
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import {
|
import {
|
||||||
ExpensesFilterSchema,
|
ExpensesFilterSchema,
|
||||||
ExpensesFilterValues,
|
ExpensesFilterValues,
|
||||||
@@ -31,64 +34,143 @@ const ExpensesFilterModal = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: ExpensesFilterModalProps) => {
|
}: ExpensesFilterModalProps) => {
|
||||||
|
const [selectedLocationId, setSelectedLocationId] = useState<string>(
|
||||||
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
|
);
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const categoryOptions = [
|
||||||
|
{ value: 'BOP', label: 'BOP' },
|
||||||
|
{ value: 'NON-BOP', label: 'NON-BOP' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const approvalStatusOptions = [
|
||||||
|
{ value: 'HEAD_AREA', label: 'Approval Head Area' },
|
||||||
|
{ value: 'UNIT_VICE_PRESIDENT', label: 'Approval Unit Vice President' },
|
||||||
|
{ value: 'FINANCE', label: 'Approval Finance' },
|
||||||
|
{ value: 'REALISASI', label: 'Realisasi' },
|
||||||
|
{ value: 'SELESAI', label: 'Selesai' },
|
||||||
|
{ value: 'DITOLAK', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const realizationStatusOptions = [
|
||||||
|
{ value: 'NOT_REALIZED', label: 'Belum Realisasi' },
|
||||||
|
{ value: 'REALIZED', label: 'Sudah Realisasi' },
|
||||||
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setVendorInputValue,
|
setInputValue: setVendorInputValue,
|
||||||
options: vendorOptions,
|
options: vendorOptions,
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
|
loadMore: loadMoreVendors,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProjectFlockInputValue,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
options: projectFlockOptions,
|
||||||
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
location_id: selectedLocationId || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const formik = useFormik<ExpensesFilterValues>({
|
const formik = useFormik<ExpensesFilterValues>({
|
||||||
|
enableReinitialize: true,
|
||||||
initialValues: initialValues || {
|
initialValues: initialValues || {
|
||||||
transaction_date: null,
|
transaction_date: null,
|
||||||
realization_date: null,
|
realization_date: null,
|
||||||
location_id: null,
|
location: null,
|
||||||
vendor_id: null,
|
vendor: null,
|
||||||
|
category: null,
|
||||||
|
approval_status: null,
|
||||||
|
realization_status: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
},
|
},
|
||||||
validationSchema: ExpensesFilterSchema,
|
validationSchema: ExpensesFilterSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
onSubmit?.(values);
|
onSubmit?.(values);
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
onReset?.();
|
|
||||||
closeModalHandler();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const locationValue = formik.values.location_id
|
useEffect(() => {
|
||||||
? locationOptions.find(
|
setSelectedLocationId(
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
) || null
|
);
|
||||||
: null;
|
}, [initialValues?.location]);
|
||||||
|
|
||||||
const vendorValue = formik.values.vendor_id
|
const { resetForm } = formik;
|
||||||
? vendorOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.vendor_id
|
const formikResetHandler = useCallback(() => {
|
||||||
) || null
|
resetForm({
|
||||||
: null;
|
values: {
|
||||||
|
transaction_date: null,
|
||||||
|
realization_date: null,
|
||||||
|
location: null,
|
||||||
|
vendor: null,
|
||||||
|
category: null,
|
||||||
|
approval_status: null,
|
||||||
|
realization_status: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSelectedLocationId('');
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset, closeModalHandler]);
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const locationId =
|
const value = val as OptionType | null;
|
||||||
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
formik.setFieldValue('location', value);
|
||||||
formik.setFieldValue('location_id', locationId);
|
formik.setFieldValue('project_flock', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
setSelectedLocationId(value?.value ? String(value.value) : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const vendorId =
|
formik.setFieldValue('vendor', val as OptionType | null);
|
||||||
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
|
||||||
formik.setFieldValue('vendor_id', vendorId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!formik.values.project_flock ||
|
||||||
|
!projectFlocksRawData ||
|
||||||
|
!isResponseSuccess(projectFlocksRawData)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProjectFlock = projectFlocksRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.project_flock?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedProjectFlock?.kandangs?.map((item) => ({
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock, projectFlocksRawData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -98,7 +180,7 @@ const ExpensesFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formik.handleReset}
|
onReset={formikResetHandler}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -121,49 +203,41 @@ const ExpensesFilterModal = ({
|
|||||||
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<div className='flex flex-col'>
|
<DateInput
|
||||||
<span className='py-2 text-xs font-semibold'>Tanggal</span>
|
name='transaction_date'
|
||||||
<div className='flex flex-row items-center gap-1.5'>
|
label='Tanggal Transaksi'
|
||||||
<DateInput
|
placeholder='Tanggal Transaksi'
|
||||||
name='transaction_date'
|
value={formik.values.transaction_date || ''}
|
||||||
placeholder='Tanggal Transaksi'
|
onChange={formik.handleChange}
|
||||||
value={formik.values.transaction_date || ''}
|
onBlur={formik.handleBlur}
|
||||||
onChange={formik.handleChange}
|
isError={
|
||||||
onBlur={formik.handleBlur}
|
formik.touched.transaction_date &&
|
||||||
isError={
|
!!formik.errors.transaction_date
|
||||||
formik.touched.transaction_date &&
|
}
|
||||||
!!formik.errors.transaction_date
|
/>
|
||||||
}
|
|
||||||
/>
|
<DateInput
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
name='realization_date'
|
||||||
<DateInput
|
label='Tanggal Realisasi'
|
||||||
name='realization_date'
|
placeholder='Tanggal Realisasi'
|
||||||
placeholder='Tanggal Realisasi'
|
value={formik.values.realization_date || ''}
|
||||||
value={formik.values.realization_date || ''}
|
onChange={formik.handleChange}
|
||||||
onChange={formik.handleChange}
|
onBlur={formik.handleBlur}
|
||||||
onBlur={formik.handleBlur}
|
isError={
|
||||||
isError={
|
formik.touched.realization_date &&
|
||||||
formik.touched.realization_date &&
|
!!formik.errors.realization_date
|
||||||
!!formik.errors.realization_date
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{formik.touched.realization_date &&
|
|
||||||
formik.errors.realization_date && (
|
|
||||||
<span className='text-xs text-error'>
|
|
||||||
{formik.errors.realization_date}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={locationValue}
|
value={formik.values.location}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isClearable
|
isClearable
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -173,14 +247,87 @@ const ExpensesFilterModal = ({
|
|||||||
label='Vendor'
|
label='Vendor'
|
||||||
placeholder='Pilih Vendor'
|
placeholder='Pilih Vendor'
|
||||||
options={vendorOptions}
|
options={vendorOptions}
|
||||||
value={vendorValue}
|
value={formik.values.vendor}
|
||||||
onChange={vendorChangeHandler}
|
onChange={vendorChangeHandler}
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
isLoading={isLoadingVendorOptions}
|
isLoading={isLoadingVendorOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreVendors}
|
||||||
isClearable
|
isClearable
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={categoryOptions}
|
||||||
|
value={formik.values.category}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('category', val as OptionType | null)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status BOP'
|
||||||
|
placeholder='Pilih Status BOP'
|
||||||
|
options={approvalStatusOptions}
|
||||||
|
value={formik.values.approval_status}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('approval_status', val as OptionType | null)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status Pencairan'
|
||||||
|
placeholder='Pilih Status Pencairan'
|
||||||
|
options={realizationStatusOptions}
|
||||||
|
value={formik.values.realization_status}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'realization_status',
|
||||||
|
val as OptionType | null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
options={projectFlockOptions}
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue('project_flock', val as OptionType | null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
}}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
options={projectFlockKandangOptions}
|
||||||
|
value={formik.values.project_flock_kandang}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_kandang',
|
||||||
|
val as OptionType | null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.project_flock}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ import { isResponseError } from '@/lib/api-helper';
|
|||||||
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||||
|
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ const ExpenseRealizationForm = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: ExpenseRealizationFormProps) => {
|
}: ExpenseRealizationFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
|
|
||||||
const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState('');
|
const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState('');
|
||||||
|
|
||||||
@@ -64,9 +67,9 @@ const ExpenseRealizationForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast.success(createExpenseRes?.message as string);
|
toast.success(createExpenseRes?.message as string);
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
},
|
},
|
||||||
[router]
|
[initialValues?.id, returnTo, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateExpenseHandler = useCallback(
|
const updateExpenseHandler = useCallback(
|
||||||
@@ -83,9 +86,9 @@ const ExpenseRealizationForm = ({
|
|||||||
|
|
||||||
toast.success(updateExpenseRes?.message as string);
|
toast.success(updateExpenseRes?.message as string);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
router.push('/expense');
|
router.push(returnTo);
|
||||||
},
|
},
|
||||||
[router]
|
[returnTo, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formik = useFormik<ExpenseRealizationFormValues>({
|
const formik = useFormik<ExpenseRealizationFormValues>({
|
||||||
@@ -207,7 +210,7 @@ const ExpenseRealizationForm = ({
|
|||||||
|
|
||||||
// add new realizations for each kandang
|
// add new realizations for each kandang
|
||||||
kandangs.forEach((kandangItem) => {
|
kandangs.forEach((kandangItem) => {
|
||||||
if (!kandangItem.id) return;
|
if (isNaN(Number(kandangItem.id))) return;
|
||||||
|
|
||||||
const existingRealization = formik.values.realizations?.find(
|
const existingRealization = formik.values.realizations?.find(
|
||||||
(realizationItem) => realizationItem.kandang_id === kandangItem.id
|
(realizationItem) => realizationItem.kandang_id === kandangItem.id
|
||||||
@@ -258,7 +261,7 @@ const ExpenseRealizationForm = ({
|
|||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href={returnTo}
|
||||||
variant='link'
|
variant='link'
|
||||||
className='w-fit p-0 text-primary'
|
className='w-fit p-0 text-primary'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
|
|||||||
setInputValue: setNonstockInputValue,
|
setInputValue: setNonstockInputValue,
|
||||||
options: nonstockOptions,
|
options: nonstockOptions,
|
||||||
isLoadingOptions: isLoadingNonstockOptions,
|
isLoadingOptions: isLoadingNonstockOptions,
|
||||||
|
loadMore: loadMoreNonstocks,
|
||||||
} = useSelect<Nonstock>(
|
} = useSelect<Nonstock>(
|
||||||
NonstockApi.basePath,
|
NonstockApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
@@ -164,6 +165,7 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
|
|||||||
options={nonstockOptions}
|
options={nonstockOptions}
|
||||||
isLoading={isLoadingNonstockOptions}
|
isLoading={isLoadingNonstockOptions}
|
||||||
onInputChange={setNonstockInputValue}
|
onInputChange={setNonstockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreNonstocks}
|
||||||
className={{ wrapper: 'min-w-48' }}
|
className={{ wrapper: 'min-w-48' }}
|
||||||
isDisabled
|
isDisabled
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -178,12 +178,14 @@ const ExpenseRequestForm = ({
|
|||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setVendorInputValue,
|
setInputValue: setVendorInputValue,
|
||||||
options: supplierOptions,
|
options: supplierOptions,
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
|
loadMore: loadMoreSuppliers,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -408,6 +410,7 @@ const ExpenseRequestForm = ({
|
|||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.location_id && Boolean(formik.errors.location_id)
|
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||||
}
|
}
|
||||||
@@ -452,6 +455,7 @@ const ExpenseRequestForm = ({
|
|||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
isLoading={isLoadingVendorOptions}
|
isLoading={isLoadingVendorOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
|
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,8 +287,8 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
PT LUMBUNG TELUR INDONESIA
|
PT LUMBUNG TELUR INDONESIA
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={ExpensePDFStyle.companyAddress}>
|
<Text style={ExpensePDFStyle.companyAddress}>
|
||||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
|
||||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
Bandung Barat, Jawa Barat 40514
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={ExpensePDFStyle.doubleDivider} />
|
<View style={ExpensePDFStyle.doubleDivider} />
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, {
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
useCallback,
|
import {
|
||||||
useEffect,
|
CellContext,
|
||||||
useMemo,
|
ColumnDef,
|
||||||
useRef,
|
SortingState,
|
||||||
useState,
|
Updater,
|
||||||
} from 'react';
|
} from '@tanstack/react-table';
|
||||||
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -39,7 +38,7 @@ import PopoverContent from '@/components/popover/PopoverContent';
|
|||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import {
|
import {
|
||||||
FinanceTableFilterSchema,
|
FinanceTableFilterSchema,
|
||||||
FinanceTableFilterValues,
|
FinanceTableFilterValues,
|
||||||
@@ -176,9 +175,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FinanceTable = () => {
|
const FinanceTable = () => {
|
||||||
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
|
||||||
const previousPathRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -187,14 +183,18 @@ const FinanceTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
transactionTypes: '',
|
transactionTypes: '',
|
||||||
bankIds: '',
|
bankIds: '',
|
||||||
customerIds: '',
|
customerIds: '',
|
||||||
supplierIds: '',
|
supplierIds: '',
|
||||||
sortBy: '',
|
sort_by: '',
|
||||||
|
orderBy: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
|
bankNames: '',
|
||||||
|
customerNames: '',
|
||||||
|
supplierNames: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -203,10 +203,14 @@ const FinanceTable = () => {
|
|||||||
bankIds: 'bank_ids',
|
bankIds: 'bank_ids',
|
||||||
customerIds: 'customer_ids',
|
customerIds: 'customer_ids',
|
||||||
supplierIds: 'supplier_ids',
|
supplierIds: 'supplier_ids',
|
||||||
sortBy: 'sort_date',
|
sort_by: 'sort_by',
|
||||||
|
orderBy: 'sort_order',
|
||||||
startDate: 'start_date',
|
startDate: 'start_date',
|
||||||
endDate: 'end_date',
|
endDate: 'end_date',
|
||||||
},
|
},
|
||||||
|
excludeKeysFromUrl: ['bankNames', 'customerNames', 'supplierNames'],
|
||||||
|
persist: true,
|
||||||
|
storeName: 'finance-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -235,7 +239,7 @@ const FinanceTable = () => {
|
|||||||
// ===== Formik for Filter =====
|
// ===== Formik for Filter =====
|
||||||
const filterFormik = useFormik<FinanceTableFilterValues>({
|
const filterFormik = useFormik<FinanceTableFilterValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
search: searchValue,
|
search: tableFilterState.search || '',
|
||||||
transaction_types: '',
|
transaction_types: '',
|
||||||
bank_ids: '',
|
bank_ids: '',
|
||||||
customer_ids: '',
|
customer_ids: '',
|
||||||
@@ -245,29 +249,48 @@ const FinanceTable = () => {
|
|||||||
end_date: '',
|
end_date: '',
|
||||||
},
|
},
|
||||||
validationSchema: FinanceTableFilterSchema,
|
validationSchema: FinanceTableFilterSchema,
|
||||||
enableReinitialize: true,
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
onSubmit: (values) => {
|
updateFilter('search', values.search, true);
|
||||||
updateFilter('search', values.search);
|
updateFilter('transactionTypes', values.transaction_types, true);
|
||||||
setSearchValue(values.search);
|
updateFilter('bankIds', values.bank_ids, true);
|
||||||
updateFilter('transactionTypes', values.transaction_types);
|
updateFilter('customerIds', values.customer_ids, true);
|
||||||
updateFilter('bankIds', values.bank_ids);
|
updateFilter('supplierIds', values.supplier_ids, true);
|
||||||
updateFilter('customerIds', values.customer_ids);
|
updateFilter('sort_by', values.sort_by, true);
|
||||||
updateFilter('supplierIds', values.supplier_ids);
|
updateFilter('startDate', values.start_date, true);
|
||||||
updateFilter('sortBy', values.sort_by);
|
updateFilter('endDate', values.end_date, true);
|
||||||
updateFilter('startDate', values.start_date);
|
// Save display names for restoration on modal reopen
|
||||||
updateFilter('endDate', values.end_date);
|
const toNames = (val: OptionType | OptionType[] | null) =>
|
||||||
|
val
|
||||||
|
? (Array.isArray(val) ? val : [val])
|
||||||
|
.map((o) => String(o.label))
|
||||||
|
.join(',')
|
||||||
|
: '';
|
||||||
|
updateFilter('bankNames', toNames(selectedBank), true);
|
||||||
|
updateFilter('customerNames', toNames(selectedCustomerId), true);
|
||||||
|
updateFilter('supplierNames', toNames(selectedSupplierId), true);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('search', '');
|
setSelectedTransactionType(null);
|
||||||
resetSearchValue();
|
setSelectedBank(null);
|
||||||
updateFilter('transactionTypes', '');
|
setSelectedCustomerId(null);
|
||||||
updateFilter('bankIds', '');
|
setSelectedSupplierId(null);
|
||||||
updateFilter('customerIds', '');
|
setSelectedSortBy(null);
|
||||||
updateFilter('supplierIds', '');
|
updateFilter('search', '', true);
|
||||||
updateFilter('sortBy', '');
|
updateFilter('transactionTypes', '', true);
|
||||||
updateFilter('startDate', '');
|
updateFilter('bankIds', '', true);
|
||||||
updateFilter('endDate', '');
|
updateFilter('customerIds', '', true);
|
||||||
|
updateFilter('supplierIds', '', true);
|
||||||
|
updateFilter('sort_by', '', true);
|
||||||
|
updateFilter('orderBy', '', true);
|
||||||
|
updateFilter('startDate', '', true);
|
||||||
|
updateFilter('endDate', '', true);
|
||||||
|
updateFilter('bankNames', '', true);
|
||||||
|
updateFilter('customerNames', '', true);
|
||||||
|
updateFilter('supplierNames', '', true);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -320,40 +343,10 @@ const FinanceTable = () => {
|
|||||||
});
|
});
|
||||||
}, [bankOptions, bankRawData]);
|
}, [bankOptions, bankRawData]);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (tableFilterState.transactionTypes) count += 1;
|
|
||||||
if (tableFilterState.bankIds) count += 1;
|
|
||||||
if (tableFilterState.customerIds) count += 1;
|
|
||||||
if (tableFilterState.supplierIds) count += 1;
|
|
||||||
if (tableFilterState.sortBy) count += 1;
|
|
||||||
if (tableFilterState.startDate) count += 1;
|
|
||||||
if (tableFilterState.endDate) count += 1;
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [
|
|
||||||
tableFilterState.transactionTypes,
|
|
||||||
tableFilterState.bankIds,
|
|
||||||
tableFilterState.customerIds,
|
|
||||||
tableFilterState.supplierIds,
|
|
||||||
tableFilterState.sortBy,
|
|
||||||
tableFilterState.startDate,
|
|
||||||
tableFilterState.endDate,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== Handler =====
|
// ===== Handler =====
|
||||||
const searchChangeHandler = useCallback(
|
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
};
|
||||||
setSearchValue(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
},
|
|
||||||
[updateFilter, setSearchValue, setPage]
|
|
||||||
);
|
|
||||||
|
|
||||||
const transactionTypeChangeHandler = (
|
const transactionTypeChangeHandler = (
|
||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
@@ -409,6 +402,26 @@ const FinanceTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sorting: SortingState = tableFilterState.sort_by
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: tableFilterState.sort_by,
|
||||||
|
desc: tableFilterState.orderBy === 'desc',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const handleSortingChange = (updater: Updater<SortingState>) => {
|
||||||
|
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
||||||
|
if (next.length > 0) {
|
||||||
|
updateFilter('sort_by', next[0].id, true);
|
||||||
|
updateFilter('orderBy', next[0].desc ? 'desc' : 'asc', true);
|
||||||
|
} else {
|
||||||
|
updateFilter('sort_by', '', true);
|
||||||
|
updateFilter('orderBy', '', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
const endDate = filterFormik.values.end_date;
|
const endDate = filterFormik.values.end_date;
|
||||||
@@ -469,28 +482,74 @@ const FinanceTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
|
// Restore transaction types from stored comma-separated IDs
|
||||||
|
const txIds = tableFilterState.transactionTypes
|
||||||
|
? tableFilterState.transactionTypes.split(',')
|
||||||
|
: [];
|
||||||
|
const restoredTxTypes = FINANCE_TRANSACTION_TYPE_OPTIONS.filter((opt) =>
|
||||||
|
txIds.includes(String(opt.value))
|
||||||
|
);
|
||||||
|
setSelectedTransactionType(restoredTxTypes.length ? restoredTxTypes : null);
|
||||||
|
|
||||||
|
// Restore banks from stored IDs and names
|
||||||
|
const bankIdList = tableFilterState.bankIds
|
||||||
|
? tableFilterState.bankIds.split(',')
|
||||||
|
: [];
|
||||||
|
const bankNameList = tableFilterState.bankNames
|
||||||
|
? tableFilterState.bankNames.split(',')
|
||||||
|
: [];
|
||||||
|
const restoredBanks = bankIdList.map((id, i) => ({
|
||||||
|
value: id,
|
||||||
|
label: bankNameList[i] || id,
|
||||||
|
}));
|
||||||
|
setSelectedBank(restoredBanks.length ? restoredBanks : null);
|
||||||
|
|
||||||
|
// Restore customers from stored IDs and names
|
||||||
|
const customerIdList = tableFilterState.customerIds
|
||||||
|
? tableFilterState.customerIds.split(',')
|
||||||
|
: [];
|
||||||
|
const customerNameList = tableFilterState.customerNames
|
||||||
|
? tableFilterState.customerNames.split(',')
|
||||||
|
: [];
|
||||||
|
const restoredCustomers = customerIdList.map((id, i) => ({
|
||||||
|
value: id,
|
||||||
|
label: customerNameList[i] || id,
|
||||||
|
}));
|
||||||
|
setSelectedCustomerId(restoredCustomers.length ? restoredCustomers : null);
|
||||||
|
|
||||||
|
// Restore suppliers from stored IDs and names
|
||||||
|
const supplierIdList = tableFilterState.supplierIds
|
||||||
|
? tableFilterState.supplierIds.split(',')
|
||||||
|
: [];
|
||||||
|
const supplierNameList = tableFilterState.supplierNames
|
||||||
|
? tableFilterState.supplierNames.split(',')
|
||||||
|
: [];
|
||||||
|
const restoredSuppliers = supplierIdList.map((id, i) => ({
|
||||||
|
value: id,
|
||||||
|
label: supplierNameList[i] || id,
|
||||||
|
}));
|
||||||
|
setSelectedSupplierId(restoredSuppliers.length ? restoredSuppliers : null);
|
||||||
|
|
||||||
|
// Restore sort by
|
||||||
|
const restoredSortBy =
|
||||||
|
sortByOptions.find(
|
||||||
|
(opt) => String(opt.value) === tableFilterState.sort_by
|
||||||
|
) || null;
|
||||||
|
setSelectedSortBy(restoredSortBy);
|
||||||
|
|
||||||
|
// Restore formik values
|
||||||
|
filterFormik.setValues({
|
||||||
|
search: tableFilterState.search || '',
|
||||||
|
transaction_types: tableFilterState.transactionTypes || '',
|
||||||
|
bank_ids: tableFilterState.bankIds || '',
|
||||||
|
customer_ids: tableFilterState.customerIds || '',
|
||||||
|
supplier_ids: tableFilterState.supplierIds || '',
|
||||||
|
sort_by: tableFilterState.sort_by || '',
|
||||||
|
start_date: tableFilterState.startDate || '',
|
||||||
|
end_date: tableFilterState.endDate || '',
|
||||||
|
});
|
||||||
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
filterFormik.validateForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetFilterHandler = () => {
|
|
||||||
setSelectedTransactionType(null);
|
|
||||||
setSelectedBank(null);
|
|
||||||
setSelectedCustomerId(null);
|
|
||||||
setSelectedSupplierId(null);
|
|
||||||
setSelectedSortBy(null);
|
|
||||||
|
|
||||||
filterFormik.resetForm();
|
|
||||||
|
|
||||||
updateFilter('search', '');
|
|
||||||
resetSearchValue();
|
|
||||||
updateFilter('transactionTypes', '');
|
|
||||||
updateFilter('bankIds', '');
|
|
||||||
updateFilter('customerIds', '');
|
|
||||||
updateFilter('supplierIds', '');
|
|
||||||
updateFilter('sortBy', '');
|
|
||||||
updateFilter('startDate', '');
|
|
||||||
updateFilter('endDate', '');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -509,10 +568,12 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'ID',
|
header: 'ID',
|
||||||
accessorKey: 'payment_code',
|
accessorKey: 'payment_code',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'References Number',
|
header: 'References Number',
|
||||||
accessorKey: 'reference_number',
|
accessorKey: 'reference_number',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.reference_number;
|
const value = props.row.original.reference_number;
|
||||||
return <span>{value ?? '-'}</span>;
|
return <span>{value ?? '-'}</span>;
|
||||||
@@ -521,6 +582,7 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Jenis Transaksi',
|
header: 'Jenis Transaksi',
|
||||||
accessorKey: 'transaction_type',
|
accessorKey: 'transaction_type',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.transaction_type
|
const value = props.row.original.transaction_type
|
||||||
.split('_')
|
.split('_')
|
||||||
@@ -530,7 +592,8 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pihak',
|
header: 'Pihak',
|
||||||
accessorFn: (finance: Finance) => finance.party?.name,
|
accessorKey: 'customer_name',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
if (props.row.original.party?.id) {
|
if (props.row.original.party?.id) {
|
||||||
return <span>{props.row.original.party?.name}</span>;
|
return <span>{props.row.original.party?.name}</span>;
|
||||||
@@ -539,13 +602,23 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal',
|
header: 'Tanggal Pembayaran',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'payment_date',
|
||||||
formatDate(finance.payment_date, 'DD MMM YYYY'),
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatDate(props.row.original.payment_date, 'DD MMM YYYY'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tanggal Dibuat',
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatDate(props.row.original.created_at, 'DD MMM YYYY'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Metode Pembayaran',
|
header: 'Metode Pembayaran',
|
||||||
accessorKey: 'payment_method',
|
accessorKey: 'payment_method',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.payment_method.split('_').join(' ');
|
const value = props.row.original.payment_method.split('_').join(' ');
|
||||||
return <span>{formatTitleCase(value)}</span>;
|
return <span>{formatTitleCase(value)}</span>;
|
||||||
@@ -553,20 +626,26 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Bank',
|
header: 'Bank',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'bank',
|
||||||
finance.bank
|
enableSorting: true,
|
||||||
? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`
|
cell: (props) =>
|
||||||
|
props.row.original.bank
|
||||||
|
? `${props.row.original.bank?.alias} - ${props.row.original.bank?.account_number} - ${props.row.original.bank?.owner}`
|
||||||
: '-',
|
: '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pengeluaran (Rp)',
|
header: 'Pengeluaran (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'expense_amount',
|
||||||
formatCurrency(Math.abs(finance.expense_amount)),
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatCurrency(Math.abs(props.row.original.expense_amount)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pemasukan (Rp)',
|
header: 'Pemasukan (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'income_amount',
|
||||||
formatCurrency(Math.abs(finance.income_amount)),
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatCurrency(Math.abs(props.row.original.income_amount)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
@@ -605,27 +684,6 @@ const FinanceTable = () => {
|
|||||||
};
|
};
|
||||||
}, [dateErrorShown]);
|
}, [dateErrorShown]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
previousPathRef.current = window.location.pathname;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
|
|
||||||
const isCurrentPathFinance = currentPath.includes('/finance');
|
|
||||||
const isPreviousPathFinance =
|
|
||||||
previousPathRef.current?.includes('/finance');
|
|
||||||
|
|
||||||
if (isPreviousPathFinance && !isCurrentPathFinance) {
|
|
||||||
resetSearchValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [resetSearchValue, dateErrorShown]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
@@ -687,25 +745,20 @@ const FinanceTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={tableFilterState}
|
||||||
color='none'
|
excludeFields={[
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'search',
|
||||||
|
'orderBy',
|
||||||
|
'bankNames',
|
||||||
|
'customerNames',
|
||||||
|
'supplierNames',
|
||||||
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className={cn(
|
className='px-3 py-2.5'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
/>
|
||||||
{
|
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -741,6 +794,9 @@ const FinanceTable = () => {
|
|||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn('p-3 mb-0'),
|
containerClassName: cn('p-3 mb-0'),
|
||||||
headerColumnClassName: 'text-nowrap',
|
headerColumnClassName: 'text-nowrap',
|
||||||
@@ -874,19 +930,9 @@ const FinanceTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
|
||||||
filterFormik.resetForm();
|
|
||||||
setSelectedTransactionType(null);
|
|
||||||
setSelectedBank(null);
|
|
||||||
setSelectedCustomerId(null);
|
|
||||||
setSelectedSupplierId(null);
|
|
||||||
setSelectedSortBy(null);
|
|
||||||
resetFilterHandler();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||||
@@ -25,7 +24,10 @@ import { cn, formatNumber, formatDate, formatCurrency } from '@/lib/helper';
|
|||||||
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||||
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
||||||
@@ -38,27 +40,89 @@ import {
|
|||||||
AdjustmentFilterType,
|
AdjustmentFilterType,
|
||||||
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
|
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
const RowOptionsMenu = ({
|
||||||
|
popoverPosition = 'bottom',
|
||||||
|
props,
|
||||||
|
deleteClickHandler,
|
||||||
|
}: {
|
||||||
|
popoverPosition: 'bottom' | 'top';
|
||||||
|
props: CellContext<InventoryAdjustment, unknown>;
|
||||||
|
deleteClickHandler: () => void;
|
||||||
|
}) => {
|
||||||
|
const popoverId = `adjustment#${props.row.original.id}`;
|
||||||
|
const popoverAnchorName = `--anchor-adjustment#${props.row.original.id}`;
|
||||||
|
|
||||||
|
const closePopover = () => {
|
||||||
|
document.getElementById(popoverId)?.hidePopover();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative'>
|
||||||
|
<PopoverButton
|
||||||
|
tabIndex={0}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
popoverTarget={popoverId}
|
||||||
|
anchorName={popoverAnchorName}
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||||
|
</PopoverButton>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
id={popoverId}
|
||||||
|
anchorName={popoverAnchorName}
|
||||||
|
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||||
|
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||||
|
<RequirePermission permissions='lti.inventory.delete'>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
deleteClickHandler();
|
||||||
|
closePopover();
|
||||||
|
}}
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const InventoryAdjustmentTable = () => {
|
const InventoryAdjustmentTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
productCategorySort: string;
|
||||||
|
productSort: string;
|
||||||
|
warehouseSort: string;
|
||||||
|
stockSort: string;
|
||||||
|
productFilter?: OptionType<string>;
|
||||||
|
warehouseFilter?: OptionType<string>;
|
||||||
|
transactionTypeFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
productCategorySort: '',
|
productCategorySort: '',
|
||||||
productSort: '',
|
productSort: '',
|
||||||
warehouseSort: '',
|
warehouseSort: '',
|
||||||
stockSort: '',
|
stockSort: '',
|
||||||
productFilter: '',
|
productFilter: undefined,
|
||||||
warehouseFilter: '',
|
warehouseFilter: undefined,
|
||||||
transactionTypeFilter: '',
|
transactionTypeFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -71,6 +135,8 @@ const InventoryAdjustmentTable = () => {
|
|||||||
warehouseFilter: 'warehouse_id',
|
warehouseFilter: 'warehouse_id',
|
||||||
transactionTypeFilter: 'transaction_type',
|
transactionTypeFilter: 'transaction_type',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'inventory-adjustment-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -79,22 +145,27 @@ const InventoryAdjustmentTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<AdjustmentFilterType>({
|
const formik = useFormik<AdjustmentFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product_id: null,
|
product: tableFilterState.productFilter,
|
||||||
warehouse_id: null,
|
warehouse: tableFilterState.warehouseFilter,
|
||||||
transaction_type: null,
|
transaction_type: tableFilterState.transactionTypeFilter,
|
||||||
},
|
},
|
||||||
validationSchema: AdjustmentFilterSchema,
|
validationSchema: AdjustmentFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productFilter', values.product_id || '');
|
updateFilter('productFilter', values.product || undefined, true);
|
||||||
updateFilter('warehouseFilter', values.warehouse_id || '');
|
updateFilter('warehouseFilter', values.warehouse || undefined, true);
|
||||||
updateFilter('transactionTypeFilter', values.transaction_type || '');
|
updateFilter(
|
||||||
|
'transactionTypeFilter',
|
||||||
|
values.transaction_type || undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('productFilter', '');
|
updateFilter('productFilter', undefined, true);
|
||||||
updateFilter('warehouseFilter', '');
|
updateFilter('warehouseFilter', undefined, true);
|
||||||
updateFilter('transactionTypeFilter', '');
|
updateFilter('transactionTypeFilter', undefined, true);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,85 +204,68 @@ const InventoryAdjustmentTable = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductChange = useCallback(
|
const handleFilterProductChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('product', val);
|
||||||
const product = val as OptionType | null;
|
};
|
||||||
const productId = product?.value ? String(product.value) : null;
|
|
||||||
formik.setFieldValue('product_id', productId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterWarehouseChange = useCallback(
|
const handleFilterWarehouseChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const warehouse = val as OptionType | null;
|
) => {
|
||||||
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
|
formik.setFieldValue('warehouse', val);
|
||||||
formik.setFieldValue('warehouse_id', warehouseId);
|
};
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterTransactionTypeChange = useCallback(
|
const handleFilterTransactionTypeChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const type = val as OptionType | null;
|
) => {
|
||||||
const typeValue = type?.value ? String(type.value) : null;
|
formik.setFieldValue('transaction_type', val);
|
||||||
formik.setFieldValue('transaction_type', typeValue);
|
};
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const productIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.product_id) return null;
|
|
||||||
return (
|
|
||||||
productOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.product_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.product_id, productOptions]);
|
|
||||||
|
|
||||||
const warehouseIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.warehouse_id) return null;
|
|
||||||
return (
|
|
||||||
warehouseOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.warehouse_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.warehouse_id, warehouseOptions]);
|
|
||||||
|
|
||||||
const transactionTypeValue = useMemo(() => {
|
|
||||||
if (!formik.values.transaction_type) return null;
|
|
||||||
return (
|
|
||||||
transactionTypeOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.transaction_type
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.transaction_type, transactionTypeOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
|
formik.setValues({
|
||||||
|
product: tableFilterState.productFilter ?? undefined,
|
||||||
|
warehouse: tableFilterState.warehouseFilter ?? undefined,
|
||||||
|
transaction_type: tableFilterState.transactionTypeFilter ?? undefined,
|
||||||
|
});
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
formik.validateForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: inventoryAdjustments, isLoading } = useSWR(
|
const {
|
||||||
|
data: inventoryAdjustments,
|
||||||
|
isLoading,
|
||||||
|
mutate: refreshAdjustments,
|
||||||
|
} = useSWR(
|
||||||
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
||||||
InventoryAdjustmentApi.getAllFetcher
|
InventoryAdjustmentApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const singleDeleteHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
const response = await InventoryAdjustmentApi.delete(
|
||||||
|
selectedAdjustment?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
singleDeleteModal.closeModal();
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
|
||||||
|
if (isResponseSuccess(response)) {
|
||||||
|
toast.success(response?.message || 'Successfully delete Adjustment!');
|
||||||
|
refreshAdjustments();
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message || 'Failed to delete Adjustment');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [selectedAdjustment, setSelectedAdjustment] = useState<
|
||||||
useEffect(() => {
|
InventoryAdjustment | undefined
|
||||||
updateFilter('search', searchValue);
|
>(undefined);
|
||||||
}, [searchValue, updateFilter]);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const singleDeleteModal = useModal();
|
||||||
useEffect(() => {
|
|
||||||
setTableState('inventory-adjustment-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
||||||
@@ -314,8 +368,39 @@ const InventoryAdjustmentTable = () => {
|
|||||||
header: 'Oleh',
|
header: 'Oleh',
|
||||||
accessorFn: (row) => row.created_user?.name ?? '-',
|
accessorFn: (row) => row.created_user?.name ?? '-',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Aksi',
|
||||||
|
cell: (props: CellContext<InventoryAdjustment, unknown>) => {
|
||||||
|
const currentPageSize =
|
||||||
|
props.table.getPaginationRowModel().rows.length;
|
||||||
|
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||||
|
const currentRowRelativeIndex =
|
||||||
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
|
const deleteClickHandler = () => {
|
||||||
|
setSelectedAdjustment(props.row.original);
|
||||||
|
singleDeleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RowOptionsMenu
|
||||||
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[tableFilterState.pageSize, tableFilterState.page]
|
[
|
||||||
|
tableFilterState.pageSize,
|
||||||
|
tableFilterState.page,
|
||||||
|
singleDeleteModal,
|
||||||
|
setSelectedAdjustment,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSortingFilter = useCallback(
|
const updateSortingFilter = useCallback(
|
||||||
@@ -401,6 +486,8 @@ const InventoryAdjustmentTable = () => {
|
|||||||
'productSort',
|
'productSort',
|
||||||
'warehouseSort',
|
'warehouseSort',
|
||||||
'stockSort',
|
'stockSort',
|
||||||
|
'productName',
|
||||||
|
'warehouseName',
|
||||||
]}
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
@@ -490,7 +577,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
placeholder='Pilih Produk'
|
placeholder='Pilih Produk'
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
value={productIdValue}
|
value={formik.values.product}
|
||||||
onChange={handleFilterProductChange}
|
onChange={handleFilterProductChange}
|
||||||
onInputChange={setProductInputValue}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProductOptions}
|
isLoading={isLoadingProductOptions}
|
||||||
@@ -502,7 +589,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih Gudang'
|
placeholder='Pilih Gudang'
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
value={warehouseIdValue}
|
value={formik.values.warehouse}
|
||||||
onChange={handleFilterWarehouseChange}
|
onChange={handleFilterWarehouseChange}
|
||||||
onInputChange={setWarehouseInputValue}
|
onInputChange={setWarehouseInputValue}
|
||||||
isLoading={isLoadingWarehouseOptions}
|
isLoading={isLoadingWarehouseOptions}
|
||||||
@@ -514,7 +601,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
label='Tipe Transaksi'
|
label='Tipe Transaksi'
|
||||||
placeholder='Pilih Tipe Transaksi'
|
placeholder='Pilih Tipe Transaksi'
|
||||||
options={transactionTypeOptions}
|
options={transactionTypeOptions}
|
||||||
value={transactionTypeValue}
|
value={formik.values.transaction_type}
|
||||||
onChange={handleFilterTransactionTypeChange}
|
onChange={handleFilterTransactionTypeChange}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -524,13 +611,9 @@ const InventoryAdjustmentTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
@@ -544,6 +627,21 @@ const InventoryAdjustmentTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={singleDeleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menghapus data Adjustment ini?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: singleDeleteHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const AdjustmentFilterSchema = object().shape({
|
export const AdjustmentFilterSchema = Yup.object().shape({
|
||||||
product_id: string().nullable(),
|
product: Yup.object({
|
||||||
warehouse_id: string().nullable(),
|
value: Yup.string().nullable(),
|
||||||
transaction_type: string().nullable(),
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
warehouse: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
transaction_type: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AdjustmentFilterType = {
|
export type AdjustmentFilterType = {
|
||||||
product_id: string | null;
|
product?: OptionType<string>;
|
||||||
warehouse_id: string | null;
|
warehouse?: OptionType<string>;
|
||||||
transaction_type: string | null;
|
transaction_type?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
InventoryAdjustmentFormSchema,
|
InventoryAdjustmentFormSchema,
|
||||||
InventoryAdjustmentFormValues,
|
InventoryAdjustmentFormValues,
|
||||||
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
|
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
|
||||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import {
|
import {
|
||||||
ProjectFlockApi,
|
ProjectFlockApi,
|
||||||
ProjectFlockKandangApi,
|
ProjectFlockKandangApi,
|
||||||
@@ -32,8 +32,6 @@ import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
|||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
|
||||||
import { Product } from '@/types/api/master-data/product';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
@@ -119,40 +117,19 @@ const InventoryAdjustmentForm = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { rawData: approvedProjectFlockKandangsRawData } =
|
|
||||||
useSelect<ProjectFlockKandang>(
|
|
||||||
ProjectFlockKandangApi.basePath,
|
|
||||||
'id',
|
|
||||||
'id',
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
step_name: 'Disetujui',
|
|
||||||
limit: '100',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const approvedProjectFlockKandangs = useMemo(() => {
|
|
||||||
if (
|
|
||||||
approvedProjectFlockKandangsRawData &&
|
|
||||||
'data' in approvedProjectFlockKandangsRawData
|
|
||||||
) {
|
|
||||||
return approvedProjectFlockKandangsRawData.data as ProjectFlockKandang[];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}, [approvedProjectFlockKandangsRawData]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setKandangInputValue,
|
options: projectFlockKandangOptions,
|
||||||
options: kandangOptionsFromApi,
|
loadMore: loadMoreProjectFlockKandangs,
|
||||||
isLoadingOptions: isLoadingKandangOptions,
|
setInputValue: setProjectFlockKandangInputValue,
|
||||||
loadMore: loadMoreKandangs,
|
isLoadingOptions: isLoadingProjectFlockKandangOptions,
|
||||||
} = useSelect<Kandang>(
|
} = useSelect(
|
||||||
selectedProjectFlock ? KandangApi.basePath : '',
|
selectedProjectFlock ? ProjectFlockKandangApi.basePath : '',
|
||||||
'id',
|
'kandang.id',
|
||||||
'name',
|
'kandang.name',
|
||||||
'search',
|
'search',
|
||||||
{
|
{
|
||||||
location_id: selectedProjectFlockLocationId,
|
step_name: 'Disetujui',
|
||||||
|
project_flock_id: String(selectedProjectFlock?.value),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -222,26 +199,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
return (product?.flags as string[]) || [];
|
return (product?.flags as string[]) || [];
|
||||||
}, [selectedProduct, productOptions]);
|
}, [selectedProduct, productOptions]);
|
||||||
|
|
||||||
const kandangOptions = useMemo(() => {
|
|
||||||
let options: OptionType[] = [];
|
|
||||||
|
|
||||||
if (selectedProjectFlock) {
|
|
||||||
const approvedKandangIds = approvedProjectFlockKandangs
|
|
||||||
.filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value)
|
|
||||||
.map((pfk) => pfk.kandang_id);
|
|
||||||
|
|
||||||
options = kandangOptionsFromApi.filter((kandang) =>
|
|
||||||
approvedKandangIds.includes(kandang.value as number)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}, [
|
|
||||||
selectedProjectFlock,
|
|
||||||
kandangOptionsFromApi,
|
|
||||||
approvedProjectFlockKandangs,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(
|
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(
|
||||||
() => ({
|
() => ({
|
||||||
location: null,
|
location: null,
|
||||||
@@ -693,10 +650,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
label='Kandang'
|
label='Kandang'
|
||||||
value={selectedKandang}
|
value={selectedKandang}
|
||||||
onChange={kandangChangeHandler}
|
onChange={kandangChangeHandler}
|
||||||
onInputChange={setKandangInputValue}
|
onInputChange={setProjectFlockKandangInputValue}
|
||||||
options={kandangOptions}
|
options={projectFlockKandangOptions}
|
||||||
onMenuScrollToBottom={loadMoreKandangs}
|
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
|
||||||
isLoading={isLoadingKandangOptions}
|
isLoading={isLoadingProjectFlockKandangOptions}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -20,7 +13,8 @@ import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
@@ -41,9 +35,11 @@ import {
|
|||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
|
deleteClickHandler,
|
||||||
}: {
|
}: {
|
||||||
popoverPosition: 'bottom' | 'top';
|
popoverPosition: 'bottom' | 'top';
|
||||||
props: CellContext<Movement, unknown>;
|
props: CellContext<Movement, unknown>;
|
||||||
|
deleteClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const popoverId = `movement#${props.row.original.id}`;
|
const popoverId = `movement#${props.row.original.id}`;
|
||||||
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
|
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
|
||||||
@@ -83,6 +79,20 @@ const RowOptionsMenu = ({
|
|||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.inventory.transfer.delete'>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
deleteClickHandler();
|
||||||
|
closePopover();
|
||||||
|
}}
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,20 +100,21 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MovementTable = () => {
|
const MovementTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
productFilter?: OptionType<string>;
|
||||||
|
warehouseFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
productFilter: '',
|
productFilter: undefined,
|
||||||
warehouseFilter: '',
|
warehouseFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -111,6 +122,8 @@ const MovementTable = () => {
|
|||||||
productFilter: 'product_id',
|
productFilter: 'product_id',
|
||||||
warehouseFilter: 'warehouse_id',
|
warehouseFilter: 'warehouse_id',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'movement-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -119,19 +132,20 @@ const MovementTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<MovementFilterType>({
|
const formik = useFormik<MovementFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product_id: null,
|
product: tableFilterState.productFilter,
|
||||||
warehouse_id: null,
|
warehouse: tableFilterState.warehouseFilter,
|
||||||
},
|
},
|
||||||
validationSchema: MovementFilterSchema,
|
validationSchema: MovementFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productFilter', values.product_id || '');
|
updateFilter('productFilter', values.product || undefined, true);
|
||||||
updateFilter('warehouseFilter', values.warehouse_id || '');
|
updateFilter('warehouseFilter', values.warehouse || undefined, true);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('productFilter', '');
|
updateFilter('productFilter', undefined, true);
|
||||||
updateFilter('warehouseFilter', '');
|
updateFilter('warehouseFilter', undefined, true);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,67 +176,59 @@ const MovementTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductChange = useCallback(
|
const handleFilterProductChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('product', val);
|
||||||
const product = val as OptionType | null;
|
};
|
||||||
const productId = product?.value ? String(product.value) : null;
|
|
||||||
formik.setFieldValue('product_id', productId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterWarehouseChange = useCallback(
|
const handleFilterWarehouseChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const warehouse = val as OptionType | null;
|
) => {
|
||||||
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
|
formik.setFieldValue('warehouse', val);
|
||||||
formik.setFieldValue('warehouse_id', warehouseId);
|
};
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const productIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.product_id) return null;
|
|
||||||
return (
|
|
||||||
productOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.product_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.product_id, productOptions]);
|
|
||||||
|
|
||||||
const warehouseIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.warehouse_id) return null;
|
|
||||||
return (
|
|
||||||
warehouseOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.warehouse_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.warehouse_id, warehouseOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
|
formik.setValues({
|
||||||
|
product: tableFilterState.productFilter ?? undefined,
|
||||||
|
warehouse: tableFilterState.warehouseFilter ?? undefined,
|
||||||
|
});
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
formik.validateForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [selectedMovement, setSelectedMovement] = useState<
|
||||||
|
Movement | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const singleDeleteModal = useModal();
|
||||||
|
|
||||||
const { data: movements, isLoading } = useSWR(
|
const {
|
||||||
|
data: movements,
|
||||||
|
isLoading,
|
||||||
|
mutate: refreshMovements,
|
||||||
|
} = useSWR(
|
||||||
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
||||||
MovementApi.getAllFetcher
|
MovementApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const singleDeleteHandler = async () => {
|
||||||
updateFilter('search', searchValue);
|
setIsDeleteLoading(true);
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const response = await MovementApi.delete(selectedMovement?.id as number);
|
||||||
setTableState('movement-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
singleDeleteModal.closeModal();
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
|
||||||
|
if (isResponseSuccess(response)) {
|
||||||
|
toast.success(response?.message || 'Successfully delete Movement!');
|
||||||
|
refreshMovements();
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message || 'Failed to delete Movement');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const movementColumns: ColumnDef<Movement>[] = useMemo(
|
const movementColumns: ColumnDef<Movement>[] = useMemo(
|
||||||
@@ -275,16 +281,27 @@ const MovementTable = () => {
|
|||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
|
const deleteClickHandler = () => {
|
||||||
|
setSelectedMovement(props.row.original);
|
||||||
|
singleDeleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenu
|
<RowOptionsMenu
|
||||||
props={props}
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[tableFilterState.pageSize, tableFilterState.page]
|
[
|
||||||
|
tableFilterState.pageSize,
|
||||||
|
tableFilterState.page,
|
||||||
|
singleDeleteModal,
|
||||||
|
setSelectedMovement,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -410,7 +427,7 @@ const MovementTable = () => {
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
placeholder='Pilih Produk'
|
placeholder='Pilih Produk'
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
value={productIdValue}
|
value={formik.values.product}
|
||||||
onChange={handleFilterProductChange}
|
onChange={handleFilterProductChange}
|
||||||
onInputChange={setProductInputValue}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProductOptions}
|
isLoading={isLoadingProductOptions}
|
||||||
@@ -422,7 +439,7 @@ const MovementTable = () => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih Gudang'
|
placeholder='Pilih Gudang'
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
value={warehouseIdValue}
|
value={formik.values.warehouse}
|
||||||
onChange={handleFilterWarehouseChange}
|
onChange={handleFilterWarehouseChange}
|
||||||
onInputChange={setWarehouseInputValue}
|
onInputChange={setWarehouseInputValue}
|
||||||
isLoading={isLoadingWarehouseOptions}
|
isLoading={isLoadingWarehouseOptions}
|
||||||
@@ -435,13 +452,9 @@ const MovementTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
@@ -455,6 +468,21 @@ const MovementTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={singleDeleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: singleDeleteHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const MovementFilterSchema = object().shape({
|
export const MovementFilterSchema = Yup.object().shape({
|
||||||
product_id: string().nullable(),
|
product: Yup.object({
|
||||||
warehouse_id: string().nullable(),
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
warehouse: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MovementFilterType = {
|
export type MovementFilterType = {
|
||||||
product_id: string | null;
|
product?: OptionType<string>;
|
||||||
warehouse_id: string | null;
|
warehouse?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
warehouse_id: number;
|
warehouse_id: number;
|
||||||
warehouse_name: string;
|
warehouse_name: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
transfer_available_qty?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== USE SELECT HOOKS =====
|
// ===== USE SELECT HOOKS =====
|
||||||
@@ -379,6 +380,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
warehouse_id: formik.values.source_warehouse_id
|
warehouse_id: formik.values.source_warehouse_id
|
||||||
? formik.values.source_warehouse_id.toString()
|
? formik.values.source_warehouse_id.toString()
|
||||||
: '',
|
: '',
|
||||||
|
transfer_context: 'inventory_transfer',
|
||||||
|
stock_mode: 'exclude_chickin',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -391,6 +394,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
warehouse_id: pw.warehouse.id,
|
warehouse_id: pw.warehouse.id,
|
||||||
warehouse_name: pw.warehouse.name,
|
warehouse_name: pw.warehouse.name,
|
||||||
quantity: pw.quantity,
|
quantity: pw.quantity,
|
||||||
|
transfer_available_qty: pw.transfer_available_qty,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
}, [productWarehouses]);
|
}, [productWarehouses]);
|
||||||
@@ -834,6 +838,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}, [formik.values.products, formik.values.deliveries]);
|
}, [formik.values.products, formik.values.deliveries]);
|
||||||
|
|
||||||
const getAvailableStock = useCallback(
|
const getAvailableStock = useCallback(
|
||||||
|
(productId: number) => {
|
||||||
|
if (type === 'detail') return 0;
|
||||||
|
const productWarehouse = productWarehouseOptions.find(
|
||||||
|
(pw) => pw.product_id === productId
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
productWarehouse?.transfer_available_qty ??
|
||||||
|
productWarehouse?.quantity ??
|
||||||
|
0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[productWarehouseOptions, type]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getTotalStock = useCallback(
|
||||||
(productId: number) => {
|
(productId: number) => {
|
||||||
if (type === 'detail') return 0;
|
if (type === 'detail') return 0;
|
||||||
const productWarehouse = productWarehouseOptions.find(
|
const productWarehouse = productWarehouseOptions.find(
|
||||||
@@ -844,6 +864,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
[productWarehouseOptions, type]
|
[productWarehouseOptions, type]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasAvailableQty = useCallback(
|
||||||
|
(productId: number) => {
|
||||||
|
const productWarehouse = productWarehouseOptions.find(
|
||||||
|
(pw) => pw.product_id === productId
|
||||||
|
);
|
||||||
|
return productWarehouse?.transfer_available_qty !== undefined;
|
||||||
|
},
|
||||||
|
[productWarehouseOptions]
|
||||||
|
);
|
||||||
|
|
||||||
const getProductQtyBottomLabel = useCallback(
|
const getProductQtyBottomLabel = useCallback(
|
||||||
(productIdx: number) => {
|
(productIdx: number) => {
|
||||||
if (type === 'detail') return undefined;
|
if (type === 'detail') return undefined;
|
||||||
@@ -851,16 +881,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (!product || !product.product_id) return undefined;
|
if (!product || !product.product_id) return undefined;
|
||||||
|
|
||||||
const availableStock = getAvailableStock(product.product_id);
|
const availableStock = getAvailableStock(product.product_id);
|
||||||
|
const totalStock = getTotalStock(product.product_id);
|
||||||
const requestedQty = Number(product.product_qty) || 0;
|
const requestedQty = Number(product.product_qty) || 0;
|
||||||
const remainingStock = availableStock - requestedQty;
|
const remainingStock = availableStock - requestedQty;
|
||||||
|
const isAyamProduct = hasAvailableQty(product.product_id);
|
||||||
|
|
||||||
if (requestedQty > 0) {
|
if (requestedQty > 0) {
|
||||||
|
if (isAyamProduct) {
|
||||||
|
return `Sisa: ${formatNumber(remainingStock)} (Total: ${formatNumber(totalStock)})`;
|
||||||
|
}
|
||||||
return `Sisa: ${formatNumber(remainingStock)}`;
|
return `Sisa: ${formatNumber(remainingStock)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAyamProduct) {
|
||||||
|
return `Tersedia: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)})`;
|
||||||
|
}
|
||||||
|
|
||||||
return `Tersedia: ${formatNumber(availableStock)}`;
|
return `Tersedia: ${formatNumber(availableStock)}`;
|
||||||
},
|
},
|
||||||
[formik.values.products, getAvailableStock, type]
|
[
|
||||||
|
formik.values.products,
|
||||||
|
getAvailableStock,
|
||||||
|
getTotalStock,
|
||||||
|
hasAvailableQty,
|
||||||
|
type,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getDeliveryProductQtyBottomLabel = useCallback(
|
const getDeliveryProductQtyBottomLabel = useCallback(
|
||||||
@@ -922,15 +967,26 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (!product || !product.product_id) return null;
|
if (!product || !product.product_id) return null;
|
||||||
|
|
||||||
const availableStock = getAvailableStock(product.product_id);
|
const availableStock = getAvailableStock(product.product_id);
|
||||||
|
const totalStock = getTotalStock(product.product_id);
|
||||||
const requestedQty = Number(product.product_qty) || 0;
|
const requestedQty = Number(product.product_qty) || 0;
|
||||||
|
const isAyamProduct = hasAvailableQty(product.product_id);
|
||||||
|
|
||||||
if (requestedQty > availableStock) {
|
if (requestedQty > availableStock) {
|
||||||
|
if (isAyamProduct) {
|
||||||
|
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)}, terpakai untuk chickin: ${formatNumber(totalStock - availableStock)})`;
|
||||||
|
}
|
||||||
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
|
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[formik.values.products, getAvailableStock, type]
|
[
|
||||||
|
formik.values.products,
|
||||||
|
getAvailableStock,
|
||||||
|
getTotalStock,
|
||||||
|
hasAvailableQty,
|
||||||
|
type,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const validateDeliveryQty = useCallback(
|
const validateDeliveryQty = useCallback(
|
||||||
|
|||||||
@@ -4,17 +4,23 @@ import Button from '@/components/Button';
|
|||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
import { InventoryProductApi } from '@/services/api/inventory';
|
import { InventoryProductApi } from '@/services/api/inventory';
|
||||||
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import { InventoryProduct } from '@/types/api/inventory/product';
|
import { InventoryProduct } from '@/types/api/inventory/product';
|
||||||
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import * as Yup from 'yup';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import InventoryProductTableSkeleton from '@/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton';
|
import InventoryProductTableSkeleton from '@/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton';
|
||||||
@@ -71,25 +77,79 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const InventoryProductTable = () => {
|
const InventoryProductTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
categoryFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
|
categoryFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
|
categoryFilter: 'product_category_id',
|
||||||
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'inventory-product-table',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FORMIK SETUP =====
|
||||||
|
const formik = useFormik<{ category?: OptionType<string> }>({
|
||||||
|
initialValues: { category: tableFilterState.categoryFilter },
|
||||||
|
validationSchema: Yup.object().shape({
|
||||||
|
category: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
}),
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
updateFilter('categoryFilter', values.category || undefined, true);
|
||||||
|
filterModal.closeModal();
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
updateFilter('categoryFilter', undefined, true);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== CATEGORY OPTIONS =====
|
||||||
|
const {
|
||||||
|
setInputValue: setCategoryInputValue,
|
||||||
|
options: categoryOptions,
|
||||||
|
isLoadingOptions: isLoadingCategoryOptions,
|
||||||
|
loadMore: loadMoreCategories,
|
||||||
|
} = useSelect<ProductCategory>(
|
||||||
|
filterModal.open ? ProductCategoryApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
formik.setValues({
|
||||||
|
category: tableFilterState.categoryFilter ?? undefined,
|
||||||
|
});
|
||||||
|
filterModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterCategoryChange = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
formik.setFieldValue('category', val);
|
||||||
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const { data: inventoryProducts, isLoading } = useSWR(
|
const { data: inventoryProducts, isLoading } = useSWR(
|
||||||
@@ -97,17 +157,8 @@ const InventoryProductTable = () => {
|
|||||||
InventoryProductApi.getAllFetcher
|
InventoryProductApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('inventory-product-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<InventoryProduct>[] = useMemo(
|
const columns: ColumnDef<InventoryProduct>[] = useMemo(
|
||||||
@@ -182,96 +233,163 @@ const InventoryProductTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<>
|
||||||
{/* Header Section */}
|
<div className='w-full'>
|
||||||
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
{/* Header Section */}
|
||||||
{/* Action Buttons */}
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
{/* Action Buttons */}
|
||||||
<RequirePermission permissions='lti.inventory.product_stock.create'>
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
<Button
|
<RequirePermission permissions='lti.inventory.product_stock.create'>
|
||||||
href='/inventory/product/add'
|
<Button
|
||||||
color='primary'
|
href='/inventory/product/add'
|
||||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
color='primary'
|
||||||
>
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
>
|
||||||
Add Product
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
</Button>
|
Add Product
|
||||||
</RequirePermission>
|
</Button>
|
||||||
</div>
|
</RequirePermission>
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Search'
|
|
||||||
value={tableFilterState.search ?? ''}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
startAdornment={
|
|
||||||
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
|
||||||
}
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full min-w-24 max-w-3xs',
|
|
||||||
inputWrapper: 'rounded-xl! shadow-button-soft',
|
|
||||||
input:
|
|
||||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table Section */}
|
|
||||||
<div className='flex flex-col mb-4'>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
</div>
|
||||||
) : !isResponseSuccess(inventoryProducts) ||
|
|
||||||
inventoryProducts.data?.length === 0 ? (
|
{/* Search and Filter */}
|
||||||
<div className='p-3'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<InventoryProductTableSkeleton
|
<DebouncedTextInput
|
||||||
columns={columns}
|
name='search'
|
||||||
icon={
|
placeholder='Search'
|
||||||
|
value={tableFilterState.search ?? ''}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
startAdornment={
|
||||||
<Icon
|
<Icon
|
||||||
icon='heroicons:document-text'
|
icon='heroicons:magnifying-glass'
|
||||||
className='text-white'
|
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||||
|
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||||
|
input:
|
||||||
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ButtonFilter
|
||||||
|
values={tableFilterState}
|
||||||
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
|
onClick={handleFilterModalOpen}
|
||||||
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<Table<InventoryProduct>
|
|
||||||
data={
|
{/* Table Section */}
|
||||||
isResponseSuccess(inventoryProducts)
|
<div className='flex flex-col mb-4'>
|
||||||
? inventoryProducts?.data
|
{isLoading ? (
|
||||||
: []
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
}
|
<span className='loading loading-spinner loading-xl' />
|
||||||
columns={columns}
|
</div>
|
||||||
pageSize={tableFilterState.pageSize}
|
) : !isResponseSuccess(inventoryProducts) ||
|
||||||
page={
|
inventoryProducts.data?.length === 0 ? (
|
||||||
isResponseSuccess(inventoryProducts)
|
<div className='p-3'>
|
||||||
? inventoryProducts?.meta?.page
|
<InventoryProductTableSkeleton
|
||||||
: 0
|
columns={columns}
|
||||||
}
|
icon={
|
||||||
totalItems={
|
<Icon
|
||||||
isResponseSuccess(inventoryProducts)
|
icon='heroicons:document-text'
|
||||||
? inventoryProducts?.meta?.total_results
|
className='text-white'
|
||||||
: 0
|
width={20}
|
||||||
}
|
height={20}
|
||||||
onPageChange={setPage}
|
/>
|
||||||
onPageSizeChange={setPageSize}
|
}
|
||||||
isLoading={isLoading}
|
/>
|
||||||
sorting={sorting}
|
</div>
|
||||||
setSorting={setSorting}
|
) : (
|
||||||
className={{
|
<Table<InventoryProduct>
|
||||||
containerClassName: cn('p-3 mb-0'),
|
data={
|
||||||
headerColumnClassName: 'text-nowrap',
|
isResponseSuccess(inventoryProducts)
|
||||||
}}
|
? inventoryProducts?.data
|
||||||
/>
|
: []
|
||||||
)}
|
}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(inventoryProducts)
|
||||||
|
? inventoryProducts?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(inventoryProducts)
|
||||||
|
? inventoryProducts?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn('p-3 mb-0'),
|
||||||
|
headerColumnClassName: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
<Modal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
className={{
|
||||||
|
modal: 'p-0',
|
||||||
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori Produk'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={categoryOptions}
|
||||||
|
value={formik.values.category}
|
||||||
|
onChange={handleFilterCategoryChange}
|
||||||
|
onInputChange={setCategoryInputValue}
|
||||||
|
isLoading={isLoadingCategoryOptions}
|
||||||
|
isClearable
|
||||||
|
onMenuScrollToBottom={loadMoreCategories}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
variant='soft'
|
||||||
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import StockLogFilterModal from '@/components/pages/inventory/product/detail/StockLogFilterModal';
|
||||||
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
||||||
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
||||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { InventoryProduct } from '@/types/api/inventory/product';
|
import { InventoryProduct } from '@/types/api/inventory/product';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
@@ -11,17 +19,34 @@ const InventoryProductDetail = ({
|
|||||||
}: {
|
}: {
|
||||||
inventoryProduct?: InventoryProduct;
|
inventoryProduct?: InventoryProduct;
|
||||||
}) => {
|
}) => {
|
||||||
const stockLogs = useMemo(() => {
|
const filterModal = useModal();
|
||||||
return (
|
|
||||||
inventoryProduct?.product_warehouses?.flatMap((warehouse) =>
|
const { state: filterState, updateFilter } = useTableFilter<{
|
||||||
warehouse.stock_logs.map((log) => ({
|
warehouse_ids: OptionType<number>[];
|
||||||
...log,
|
}>({
|
||||||
warehouse_name: warehouse.warehouse_name,
|
initial: {
|
||||||
warehouse_id: warehouse.warehouse_id,
|
warehouse_ids: [],
|
||||||
}))
|
},
|
||||||
) || []
|
persist: true,
|
||||||
);
|
storeName: 'inventory-product-stock-log-filter',
|
||||||
}, [inventoryProduct]);
|
});
|
||||||
|
|
||||||
|
const filteredProductWarehouses = useMemo(() => {
|
||||||
|
const warehouses = inventoryProduct?.product_warehouses ?? [];
|
||||||
|
if (!filterState.warehouse_ids?.length) return warehouses;
|
||||||
|
const selectedIds = new Set(filterState.warehouse_ids.map((w) => w.value));
|
||||||
|
return warehouses.filter((pw) => selectedIds.has(pw.warehouse_id));
|
||||||
|
}, [inventoryProduct?.product_warehouses, filterState.warehouse_ids]);
|
||||||
|
|
||||||
|
const filterSubmitHandler = (values: {
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
}) => {
|
||||||
|
updateFilter('warehouse_ids', values.warehouse_ids, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterResetHandler = () => {
|
||||||
|
updateFilter('warehouse_ids', [], true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
@@ -114,7 +139,29 @@ const InventoryProductDetail = ({
|
|||||||
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
|
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StockLogTable stockLogs={stockLogs} />
|
<RequirePermission permissions={'lti.inventory.stock_log.list'}>
|
||||||
|
<div className='flex justify-end'>
|
||||||
|
<ButtonFilter
|
||||||
|
values={{ warehouse_ids: filterState.warehouse_ids }}
|
||||||
|
onClick={filterModal.openModal}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{filteredProductWarehouses.map((productWarehouse) => (
|
||||||
|
<StockLogTable
|
||||||
|
key={productWarehouse.id}
|
||||||
|
productWarehouse={productWarehouse}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<StockLogFilterModal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
productWarehouses={inventoryProduct?.product_warehouses ?? []}
|
||||||
|
initialValues={{ warehouse_ids: filterState.warehouse_ids }}
|
||||||
|
onSubmit={filterSubmitHandler}
|
||||||
|
onReset={filterResetHandler}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { RefObject, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface StockLogFilterModalProps {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
productWarehouses: ProductWarehouseStock[];
|
||||||
|
initialValues: {
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
};
|
||||||
|
onSubmit: (values: { warehouse_ids: OptionType<number>[] }) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockLogFilterModal = ({
|
||||||
|
ref,
|
||||||
|
productWarehouses,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: StockLogFilterModalProps) => {
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const warehouseOptions: OptionType<number>[] = productWarehouses.map(
|
||||||
|
(pw) => ({
|
||||||
|
label: pw.warehouse_name,
|
||||||
|
value: pw.warehouse_id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
|
onSubmit: (values) => {
|
||||||
|
onSubmit(values);
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { resetForm } = formik;
|
||||||
|
|
||||||
|
const formikResetHandler = useCallback(() => {
|
||||||
|
resetForm({ values: { warehouse_ids: [] } });
|
||||||
|
onReset();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal ref={ref} className={{ modalBox: 'p-0 rounded-xl' }}>
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formikResetHandler}
|
||||||
|
className='w-full flex flex-col'
|
||||||
|
>
|
||||||
|
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='text-sm font-medium'>Filter Stock Log</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={closeModalHandler}
|
||||||
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInputCheckbox
|
||||||
|
label='Gudang'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih gudang'
|
||||||
|
options={warehouseOptions}
|
||||||
|
value={formik.values.warehouse_ids}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('warehouse_ids', val as OptionType<number>[])
|
||||||
|
}
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 rounded-lg text-base-content/65'
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StockLogFilterModal;
|
||||||
@@ -1,95 +1,183 @@
|
|||||||
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
|
import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
|
||||||
import { StockLog } from '@/types/api/inventory/product';
|
import { StockLogApi } from '@/services/api/inventory';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ProductWarehouseStock, StockLog } from '@/types/api/inventory/product';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { FileDown } from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
|
||||||
|
warehouseName
|
||||||
|
) => [
|
||||||
|
{
|
||||||
|
header: 'ID',
|
||||||
|
accessorKey: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tanggal',
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatDate(props.row.original.created_at, 'DD-MMM-yyyy');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Gudang',
|
||||||
|
accessorKey: 'warehouse_name',
|
||||||
|
cell: warehouseName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Stock Akhir',
|
||||||
|
accessorKey: 'stock',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.stock);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Peningkatan',
|
||||||
|
accessorKey: 'increase',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.increase);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Penurunan',
|
||||||
|
accessorKey: 'decrease',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.decrease);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jenis Transaksi',
|
||||||
|
accessorKey: 'loggable_type',
|
||||||
|
cell: (props) => {
|
||||||
|
return props.row.original.loggable_type
|
||||||
|
? formatTitleCase(props.row.original.loggable_type)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Catatan',
|
||||||
|
accessorKey: 'notes',
|
||||||
|
cell: (props) => {
|
||||||
|
return props.row.original.notes ? props.row.original.notes : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Oleh',
|
||||||
|
accessorKey: 'created_user.name',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const StockLogTable = ({
|
const StockLogTable = ({
|
||||||
stockLogs,
|
productWarehouse,
|
||||||
}: {
|
}: {
|
||||||
stockLogs: (StockLog & { warehouse_name: string; warehouse_id: number })[];
|
productWarehouse: ProductWarehouseStock;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isExportLoading, setIsExportLoading] = useState(false);
|
||||||
|
const [hasBeenVisible, setHasBeenVisible] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setHasBeenVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (containerRef.current) observer.observe(containerRef.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
product_warehouse_id: productWarehouse.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleExportExcel = async () => {
|
||||||
|
setIsExportLoading(true);
|
||||||
|
try {
|
||||||
|
await StockLogApi.exportToExcel(
|
||||||
|
productWarehouse.warehouse_name,
|
||||||
|
getTableFilterQueryString()
|
||||||
|
);
|
||||||
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
|
} finally {
|
||||||
|
setIsExportLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: stockLogsResponse, isLoading: isLoadingStockLogs } = useSWR(
|
||||||
|
hasBeenVisible
|
||||||
|
? `${StockLogApi.basePath}${getTableFilterQueryString()}`
|
||||||
|
: null,
|
||||||
|
StockLogApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const stockLogs = isResponseSuccess(stockLogsResponse)
|
||||||
|
? stockLogsResponse.data
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div ref={containerRef}>
|
||||||
title='Informasi Stock Produk'
|
<Card
|
||||||
collapsible
|
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
|
||||||
variant='bordered'
|
collapsible
|
||||||
className={{
|
variant='bordered'
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table<StockLog>
|
|
||||||
data={stockLogs}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'ID',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Tanggal',
|
|
||||||
accessorKey: 'created_at',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatDate(props.row.original.created_at, 'DD-MMM-yyyy');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Gudang',
|
|
||||||
accessorKey: 'warehouse_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Stock Akhir',
|
|
||||||
accessorKey: 'stock',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.stock);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Peningkatan',
|
|
||||||
accessorKey: 'increase',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.increase);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Penurunan',
|
|
||||||
accessorKey: 'decrease',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.decrease);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Jenis Transaksi',
|
|
||||||
accessorKey: 'loggable_type',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.loggable_type
|
|
||||||
? formatTitleCase(props.row.original.loggable_type)
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Catatan',
|
|
||||||
accessorKey: 'notes',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.notes ? props.row.original.notes : '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Oleh',
|
|
||||||
accessorKey: 'created_user.name',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'mt-6',
|
wrapper: 'w-full',
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Card>
|
<div className='flex justify-end px-6 pt-4'>
|
||||||
|
<Button onClick={handleExportExcel} isLoading={isExportLoading}>
|
||||||
|
<FileDown size={16} />
|
||||||
|
Export Excel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table<StockLog>
|
||||||
|
data={stockLogs}
|
||||||
|
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
|
||||||
|
page={tableFilterState.page ?? 0}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
isLoading={isLoadingStockLogs}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(stockLogsResponse)
|
||||||
|
? stockLogsResponse.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'mt-4 mb-0',
|
||||||
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||||
|
bodyRowClassName: 'border-b border-b-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,42 @@
|
|||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { formatNumber } from '@/lib/helper';
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
const stockProductWarehouseTableColumns: ColumnDef<ProductWarehouseStock>[] = [
|
||||||
|
{
|
||||||
|
header: 'Nama Gudang',
|
||||||
|
accessorKey: 'warehouse_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Lokasi',
|
||||||
|
accessorKey: 'location',
|
||||||
|
cell: (props) => {
|
||||||
|
return props.row.original.location != null
|
||||||
|
? props.row.original.location.name
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Stok',
|
||||||
|
accessorFn(row) {
|
||||||
|
return row.current_stock;
|
||||||
|
},
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.current_stock);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const StockProductWarehouseTable = ({
|
const StockProductWarehouseTable = ({
|
||||||
productWarehouseStock,
|
productWarehouseStock,
|
||||||
}: {
|
}: {
|
||||||
productWarehouseStock?: ProductWarehouseStock[];
|
productWarehouseStock?: ProductWarehouseStock[];
|
||||||
}) => {
|
}) => {
|
||||||
|
const { state: tableFilterState, setPage, setPageSize } = useTableFilter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title='Informasi Gudang'
|
title='Informasi Gudang'
|
||||||
@@ -19,32 +48,14 @@ const StockProductWarehouseTable = ({
|
|||||||
>
|
>
|
||||||
<Table<ProductWarehouseStock>
|
<Table<ProductWarehouseStock>
|
||||||
data={productWarehouseStock ?? []}
|
data={productWarehouseStock ?? []}
|
||||||
columns={[
|
columns={stockProductWarehouseTableColumns}
|
||||||
{
|
pageSize={tableFilterState.pageSize}
|
||||||
header: 'Nama Gudang',
|
page={tableFilterState.page ?? 0}
|
||||||
accessorKey: 'warehouse_name',
|
totalItems={productWarehouseStock?.length ?? 0}
|
||||||
},
|
onPageChange={setPage}
|
||||||
{
|
onPageSizeChange={setPageSize}
|
||||||
header: 'Lokasi',
|
|
||||||
accessorKey: 'location',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.location != null
|
|
||||||
? props.row.original.location.name
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Stok',
|
|
||||||
accessorFn(row) {
|
|
||||||
return row.current_stock;
|
|
||||||
},
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.current_stock);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'mt-6',
|
containerClassName: 'mt-6 mb-0',
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
|||||||
@@ -199,6 +199,9 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
'yyyy-MM-DD'
|
'yyyy-MM-DD'
|
||||||
),
|
),
|
||||||
vehicle_number: product.vehicle_number,
|
vehicle_number: product.vehicle_number,
|
||||||
|
weight_per_convertion: parseFloat(
|
||||||
|
String(product.weight_per_convertion ?? 0)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -368,7 +371,9 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
const currentProducts = deliveryOrderValues?.find(
|
const currentProducts = deliveryOrderValues?.find(
|
||||||
(product) => product.id == id
|
(product) => product.id == id
|
||||||
);
|
);
|
||||||
setSelectedDeliveryProduct(values ?? currentProducts ?? null);
|
|
||||||
|
setSelectedDeliveryProduct(currentProducts ?? values ?? null);
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
setStep(2);
|
setStep(2);
|
||||||
}
|
}
|
||||||
@@ -430,6 +435,9 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
'yyyy-MM-DD'
|
'yyyy-MM-DD'
|
||||||
),
|
),
|
||||||
vehicle_number: product.vehicle_number,
|
vehicle_number: product.vehicle_number,
|
||||||
|
weight_per_convertion: parseFloat(
|
||||||
|
String(product.weight_per_convertion ?? 0)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -841,7 +849,11 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||||
disabled={deliveryRejected}
|
disabled={deliveryRejected}
|
||||||
>
|
>
|
||||||
Approve
|
{marketing?.data?.latest_approval?.step_number === 1 &&
|
||||||
|
'Approve'}
|
||||||
|
|
||||||
|
{marketing?.data?.latest_approval?.step_number === 2 &&
|
||||||
|
'Deliver Item'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject, useMemo } from 'react';
|
import { RefObject, useCallback, useMemo } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
@@ -10,22 +10,38 @@ import SelectInput, {
|
|||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
import {
|
||||||
|
MarketingFilterFormValues,
|
||||||
|
MarketingFilterSchema,
|
||||||
|
} from '@/components/pages/marketing/filter/MarketingFilter';
|
||||||
import { MarketingFilter } from '@/types/api/marketing/marketing';
|
import { MarketingFilter } from '@/types/api/marketing/marketing';
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { Product } from '@/types/api/master-data/product';
|
||||||
|
|
||||||
interface MarketingFilterModal {
|
interface MarketingFilterModal {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
onSubmit?: (values: MarketingFilter) => void;
|
onSubmit?: (values: MarketingFilter) => void;
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
|
initialValues?: {
|
||||||
|
product_ids: OptionType<number>[];
|
||||||
|
status: OptionType<string> | null;
|
||||||
|
customer: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarketingFilterModal = ({
|
const MarketingFilterModal = ({
|
||||||
ref,
|
ref,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
|
initialValues,
|
||||||
}: MarketingFilterModal) => {
|
}: MarketingFilterModal) => {
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
@@ -33,51 +49,35 @@ const MarketingFilterModal = ({
|
|||||||
|
|
||||||
// ===== OPTIONS =====
|
// ===== OPTIONS =====
|
||||||
const {
|
const {
|
||||||
rawData: productsRawData,
|
options: productsOptions,
|
||||||
isLoadingOptions: isLoadingProductsOptions,
|
isLoadingOptions: isLoadingProductsOptions,
|
||||||
setInputValue: setProductsInputValue,
|
setInputValue: setProductsInputValue,
|
||||||
loadMore: loadMoreProducts,
|
loadMore: loadMoreProducts,
|
||||||
} = useSelect<BaseMarketing>(MarketingApi.basePath, 'id', 'so_number', '', {
|
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
|
||||||
limit: 'limit',
|
include_all: 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
const productsOptions = useMemo(() => {
|
|
||||||
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
|
|
||||||
|
|
||||||
const productsMap = new Map<number, { value: number; label: string }>();
|
|
||||||
|
|
||||||
productsRawData.data.forEach((deliveryOrder: BaseMarketing) => {
|
|
||||||
deliveryOrder.sales_order?.forEach((so: BaseSalesOrder) => {
|
|
||||||
const product = so.product_warehouse?.product;
|
|
||||||
if (product?.id && product?.name) {
|
|
||||||
productsMap.set(product.id, {
|
|
||||||
value: product.id,
|
|
||||||
label: product.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(productsMap.values());
|
|
||||||
}, [productsRawData]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: customersOptions,
|
options: customersOptions,
|
||||||
isLoadingOptions: isLoadingCustomersOptions,
|
isLoadingOptions: isLoadingCustomersOptions,
|
||||||
setInputValue: setCustomersInputValue,
|
setInputValue: setCustomersInputValue,
|
||||||
loadMore: loadMoreCustomers,
|
loadMore: loadMoreCustomers,
|
||||||
} = useSelect(MarketingApi.basePath, 'customer.id', 'customer.name', '', {
|
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search', {
|
||||||
limit: 'limit',
|
has_marketing: 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
const uniqueCustomersOptions = useMemo(() => {
|
const {
|
||||||
const seen = new Set();
|
options: projectFlockOptions,
|
||||||
return customersOptions.filter((customer) => {
|
rawData: projectFlocksRawData,
|
||||||
if (seen.has(customer.value)) return false;
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
seen.add(customer.value);
|
setInputValue: setProjectFlockInputValue,
|
||||||
return true;
|
loadMore: loadMoreProjectFlocks,
|
||||||
});
|
} = useSelect<ProjectFlock>(
|
||||||
}, [customersOptions]);
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
...MARKETING_APPROVAL_LINE.map((item) => ({
|
...MARKETING_APPROVAL_LINE.map((item) => ({
|
||||||
@@ -87,23 +87,30 @@ const MarketingFilterModal = ({
|
|||||||
{ value: 'DITOLAK', label: 'Ditolak' },
|
{ value: 'DITOLAK', label: 'Ditolak' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const formik = useFormik<{
|
const formik = useFormik<MarketingFilterFormValues>({
|
||||||
product_ids: OptionType[];
|
initialValues: initialValues || {
|
||||||
status: OptionType | null;
|
|
||||||
customer_id: OptionType | null;
|
|
||||||
}>({
|
|
||||||
initialValues: {
|
|
||||||
product_ids: [],
|
product_ids: [],
|
||||||
status: null,
|
status: null,
|
||||||
customer_id: null,
|
customer: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
},
|
},
|
||||||
|
validationSchema: MarketingFilterSchema,
|
||||||
|
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues = {
|
const formattedValues: MarketingFilter = {
|
||||||
...values,
|
|
||||||
product_ids: values.product_ids.map((item) => Number(item.value)),
|
product_ids: values.product_ids.map((item) => Number(item.value)),
|
||||||
|
product_names: values.product_ids.map((item) => item.label),
|
||||||
status: values.status?.value.toString() || '',
|
status: values.status?.value.toString() || '',
|
||||||
customer_id: Number(values.customer_id?.value),
|
status_name: values.status?.label || '-',
|
||||||
|
customer_id: Number(values.customer?.value),
|
||||||
|
customer_name: values.customer?.label || '-',
|
||||||
|
project_flock_id: values.project_flock?.value || undefined,
|
||||||
|
project_flock_name: values.project_flock?.label,
|
||||||
|
project_flock_kandang_id:
|
||||||
|
Number(values.project_flock_kandang?.value) || undefined,
|
||||||
|
project_flock_kandang_name:
|
||||||
|
values.project_flock_kandang?.label || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit?.(formattedValues);
|
onSubmit?.(formattedValues);
|
||||||
@@ -116,18 +123,58 @@ const MarketingFilterModal = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { resetForm } = formik;
|
||||||
|
|
||||||
|
const formikResetHandler = useCallback(() => {
|
||||||
|
resetForm({
|
||||||
|
values: {
|
||||||
|
product_ids: [],
|
||||||
|
status: null,
|
||||||
|
customer: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset, closeModalHandler]);
|
||||||
|
|
||||||
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('product_ids', val as OptionType[]);
|
formik.setFieldValue('product_ids', val as OptionType[]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('customer_id', val as OptionType);
|
formik.setFieldValue(
|
||||||
|
'customer',
|
||||||
|
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('status', val as OptionType);
|
formik.setFieldValue('status', val as OptionType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!formik.values.project_flock ||
|
||||||
|
!projectFlocksRawData ||
|
||||||
|
!isResponseSuccess(projectFlocksRawData)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProjectFlock = projectFlocksRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.project_flock?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedProjectFlock?.kandangs?.map((item) => ({
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock, projectFlocksRawData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -137,7 +184,7 @@ const MarketingFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formik.handleReset}
|
onReset={formikResetHandler}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -187,13 +234,44 @@ const MarketingFilterModal = ({
|
|||||||
label='Customer'
|
label='Customer'
|
||||||
isClearable
|
isClearable
|
||||||
placeholder='Pilih customer'
|
placeholder='Pilih customer'
|
||||||
options={uniqueCustomersOptions}
|
options={customersOptions}
|
||||||
isLoading={isLoadingCustomersOptions}
|
isLoading={isLoadingCustomersOptions}
|
||||||
value={formik.values.customer_id}
|
value={formik.values.customer}
|
||||||
onChange={customerChangeHandler}
|
onChange={customerChangeHandler}
|
||||||
onInputChange={setCustomersInputValue}
|
onInputChange={setCustomersInputValue}
|
||||||
onMenuScrollToBottom={loadMoreCustomers}
|
onMenuScrollToBottom={loadMoreCustomers}
|
||||||
/>
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
options={projectFlockOptions}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock',
|
||||||
|
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
||||||
|
);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
}}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
options={projectFlockKandangOptions}
|
||||||
|
value={formik.values.project_flock_kandang}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_kandang',
|
||||||
|
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isDisabled={!formik.values.project_flock}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
|
|||||||
@@ -2,26 +2,39 @@
|
|||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import TextArea from '@/components/input/TextArea';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import {
|
||||||
|
getErrorMessage,
|
||||||
|
isResponseError,
|
||||||
|
isResponseSuccess,
|
||||||
|
} from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
MarketingApi,
|
MarketingApi,
|
||||||
SalesOrderApi,
|
SalesOrderApi,
|
||||||
} from '@/services/api/marketing/marketing';
|
} from '@/services/api/marketing/marketing';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import {
|
import {
|
||||||
BaseSalesOrder,
|
BaseSalesOrder,
|
||||||
Marketing,
|
Marketing,
|
||||||
MarketingFilter,
|
MarketingFilter,
|
||||||
} from '@/types/api/marketing/marketing';
|
} from '@/types/api/marketing/marketing';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { CellContext, ColumnDef, Row } from '@tanstack/react-table';
|
import {
|
||||||
|
CellContext,
|
||||||
|
ColumnDef,
|
||||||
|
Row,
|
||||||
|
SortingState,
|
||||||
|
Updater,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useMemo, useState } from 'react';
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
@@ -154,12 +167,21 @@ const MarketingTable = () => {
|
|||||||
);
|
);
|
||||||
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
const [bulkDeliveryDate, setBulkDeliveryDate] = useState('');
|
||||||
|
const [bulkDeliveryNotes, setBulkDeliveryNotes] = useState('');
|
||||||
|
const [isSubmittingBulkDelivery, setIsSubmittingBulkDelivery] =
|
||||||
|
useState(false);
|
||||||
|
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||||
|
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
||||||
|
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const confirmationModal = useModal();
|
const confirmationModal = useModal();
|
||||||
const productsModal = useModal();
|
const productsModal = useModal();
|
||||||
const deliveryModal = useModal();
|
const deliveryModal = useModal();
|
||||||
|
const bulkDeliveryModal = useModal();
|
||||||
|
const exportProgressInputModal = useModal();
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -172,8 +194,17 @@ const MarketingTable = () => {
|
|||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
product_ids: '',
|
product_ids: '',
|
||||||
|
product_names: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
status_name: '',
|
||||||
customer_id: '',
|
customer_id: '',
|
||||||
|
customer_name: '',
|
||||||
|
project_flock_id: '',
|
||||||
|
project_flock_name: '',
|
||||||
|
project_flock_kandang_id: '',
|
||||||
|
project_flock_kandang_name: '',
|
||||||
|
sort_by: '',
|
||||||
|
order_by: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -181,9 +212,43 @@ const MarketingTable = () => {
|
|||||||
product_ids: 'product_ids',
|
product_ids: 'product_ids',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
customer_id: 'customer_id',
|
customer_id: 'customer_id',
|
||||||
|
project_flock_id: 'project_flock_id',
|
||||||
|
project_flock_kandang_id: 'project_flock_kandang_id',
|
||||||
|
sort_by: 'sort_by',
|
||||||
|
order_by: 'sort_order',
|
||||||
},
|
},
|
||||||
|
excludeKeysFromUrl: [
|
||||||
|
'product_names',
|
||||||
|
'status_name',
|
||||||
|
'customer_name',
|
||||||
|
'project_flock_name',
|
||||||
|
'project_flock_kandang_name',
|
||||||
|
],
|
||||||
|
|
||||||
|
persist: true,
|
||||||
|
storeName: 'marketing-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sorting: SortingState = tableFilterState.sort_by
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: tableFilterState.sort_by,
|
||||||
|
desc: tableFilterState.order_by === 'desc',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const handleSortingChange = (updater: Updater<SortingState>) => {
|
||||||
|
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
||||||
|
if (next.length > 0) {
|
||||||
|
updateFilter('sort_by', next[0].id, true);
|
||||||
|
updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true);
|
||||||
|
} else {
|
||||||
|
updateFilter('sort_by', '', true);
|
||||||
|
updateFilter('order_by', '', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
data: marketing,
|
data: marketing,
|
||||||
@@ -198,26 +263,64 @@ const MarketingTable = () => {
|
|||||||
const filterSubmitHandler = (values: MarketingFilter) => {
|
const filterSubmitHandler = (values: MarketingFilter) => {
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'product_ids',
|
'product_ids',
|
||||||
values.product_ids?.map((item) => item.toString()).join(',')
|
values.product_ids?.map((item) => item.toString()).join(','),
|
||||||
|
true
|
||||||
);
|
);
|
||||||
updateFilter('status', values.status ? values.status.toString() : '');
|
updateFilter('product_names', values.product_names?.join(','));
|
||||||
|
updateFilter('status', values.status ? values.status.toString() : '', true);
|
||||||
|
updateFilter('status_name', values.status_name, true);
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'customer_id',
|
'customer_id',
|
||||||
values.customer_id ? values.customer_id.toString() : ''
|
values.customer_id ? values.customer_id.toString() : '',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
updateFilter('customer_name', values.customer_name, true);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_id',
|
||||||
|
values.project_flock_id ? values.project_flock_id.toString() : '',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
updateFilter('project_flock_name', values.project_flock_name ?? '', true);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_kandang_id',
|
||||||
|
values.project_flock_kandang_id
|
||||||
|
? values.project_flock_kandang_id.toString()
|
||||||
|
: '',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_kandang_name',
|
||||||
|
values.project_flock_kandang_name ?? '',
|
||||||
|
true
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
|
const [isDeliveryLoading, setIsDeliveryLoading] = useState(false);
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
updateFilter('product_ids', '');
|
updateFilter('product_ids', '', true);
|
||||||
updateFilter('status', '');
|
updateFilter('product_names', '', true);
|
||||||
updateFilter('customer_id', '');
|
updateFilter('status', '', true);
|
||||||
|
updateFilter('status_name', '', true);
|
||||||
|
updateFilter('customer_id', '', true);
|
||||||
|
updateFilter('customer_name', '', true);
|
||||||
|
updateFilter('project_flock_id', '', true);
|
||||||
|
updateFilter('project_flock_name', '', true);
|
||||||
|
updateFilter('project_flock_kandang_id', '', true);
|
||||||
|
updateFilter('project_flock_kandang_name', '', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
const approveClickHandler = () => {
|
||||||
setApproveAction('APPROVED');
|
setApproveAction('APPROVED');
|
||||||
|
|
||||||
|
if (selectedApprovalStep === 2) {
|
||||||
|
bulkDeliveryModal.openModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -226,10 +329,13 @@ const MarketingTable = () => {
|
|||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const productsClickHandler = (item: Marketing) => {
|
const productsClickHandler = useCallback(
|
||||||
setSelectedItem(item);
|
(item: Marketing) => {
|
||||||
productsModal.openModal();
|
setSelectedItem(item);
|
||||||
};
|
productsModal.openModal();
|
||||||
|
},
|
||||||
|
[productsModal]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteMarketingHandler = async () => {
|
const deleteMarketingHandler = async () => {
|
||||||
const deleteMarketingRes = await MarketingApi.delete(
|
const deleteMarketingRes = await MarketingApi.delete(
|
||||||
@@ -251,75 +357,226 @@ const MarketingTable = () => {
|
|||||||
const selectedRowsData = allData.filter(
|
const selectedRowsData = allData.filter(
|
||||||
(row) => rowSelection[row.id.toString()]
|
(row) => rowSelection[row.id.toString()]
|
||||||
);
|
);
|
||||||
|
const selectedApprovalStep =
|
||||||
|
selectedRowsData.length > 0
|
||||||
|
? selectedRowsData[0].latest_approval.step_number
|
||||||
|
: null;
|
||||||
|
|
||||||
const hasApprovable = selectedRowsData.some(
|
const eligibleSelectedRows = selectedRowsData.filter((row) => {
|
||||||
(row) =>
|
const approval = row.latest_approval;
|
||||||
row.latest_approval.step_number === 1 &&
|
|
||||||
row.latest_approval.action !== 'REJECTED'
|
if (approval.action === 'REJECTED') {
|
||||||
);
|
return false;
|
||||||
const hasRejectable = selectedRowsData.some(
|
}
|
||||||
(row) =>
|
|
||||||
row.latest_approval.step_number === 1 &&
|
if (selectedApprovalStep === null) {
|
||||||
row.latest_approval.action !== 'REJECTED'
|
return approval.step_number === 1 || approval.step_number === 2;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return approval.step_number === selectedApprovalStep;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasApprovable = eligibleSelectedRows.length > 0;
|
||||||
|
const hasRejectable = eligibleSelectedRows.length > 0;
|
||||||
|
|
||||||
const disableApprove = !hasApprovable;
|
const disableApprove = !hasApprovable;
|
||||||
const disableReject = !hasRejectable;
|
const disableReject = !hasRejectable;
|
||||||
|
|
||||||
const idsToProcess =
|
const idsToProcess = eligibleSelectedRows.map((row) => row.id);
|
||||||
approveAction === 'APPROVED'
|
const nextApprovalStatus =
|
||||||
? selectedRowsData
|
selectedApprovalStep === 1
|
||||||
.filter((row) => row.latest_approval.step_number === 1)
|
? 'SALES_ORDER'
|
||||||
.map((row) => row.id)
|
: selectedApprovalStep === 2
|
||||||
: selectedRowsData
|
? 'DELIVERY_ORDER'
|
||||||
.filter((row) => row.latest_approval.step_number === 2)
|
: null;
|
||||||
.map((row) => row.id);
|
|
||||||
|
const productIds = tableFilterState.product_ids
|
||||||
|
? tableFilterState.product_ids
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const productLabels = tableFilterState.product_names
|
||||||
|
? tableFilterState.product_names
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const marketingFilterInitialValues = {
|
||||||
|
product_ids: productIds.map((value, idx) => ({
|
||||||
|
value: Number(value),
|
||||||
|
label: productLabels[idx] || '-',
|
||||||
|
})),
|
||||||
|
status: tableFilterState.status
|
||||||
|
? {
|
||||||
|
value: tableFilterState.status,
|
||||||
|
label: tableFilterState.status_name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
customer: tableFilterState.customer_id
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.customer_id),
|
||||||
|
label: tableFilterState.customer_name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
project_flock: tableFilterState.project_flock_id
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.project_flock_id),
|
||||||
|
label: tableFilterState.project_flock_name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
project_flock_kandang: tableFilterState.project_flock_kandang_id
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.project_flock_kandang_id),
|
||||||
|
label: tableFilterState.project_flock_kandang_name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
const approveMarketingHandler = async (notes: string) => {
|
const approveMarketingHandler = async (notes: string) => {
|
||||||
let idsToProcess: number[] = [];
|
|
||||||
|
|
||||||
idsToProcess = selectedRowsData
|
|
||||||
.filter((row) => row.latest_approval.step_number === 1)
|
|
||||||
.map((row) => row.id);
|
|
||||||
|
|
||||||
if (idsToProcess.length === 0) {
|
if (idsToProcess.length === 0) {
|
||||||
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
||||||
confirmationModal.closeModal();
|
confirmationModal.closeModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const approveMarketingRes = await SalesOrderApi.bulkApprovals(
|
if (approveAction === 'APPROVED' && selectedApprovalStep !== 1) {
|
||||||
idsToProcess,
|
toast.error('Approve tahap ini harus menggunakan tanggal pengiriman.');
|
||||||
approveAction,
|
confirmationModal.closeModal();
|
||||||
notes
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
if (isResponseSuccess(approveMarketingRes)) {
|
if (approveAction === 'APPROVED' && !nextApprovalStatus) {
|
||||||
|
toast.error('Status approval berikutnya tidak valid.');
|
||||||
confirmationModal.closeModal();
|
confirmationModal.closeModal();
|
||||||
toast.success(approveMarketingRes?.message as string);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const approveMarketingRes: BaseApiResponse<unknown> | undefined =
|
||||||
|
approveAction === 'APPROVED'
|
||||||
|
? await MarketingApi.bulkApprovals(
|
||||||
|
idsToProcess,
|
||||||
|
nextApprovalStatus as 'SALES_ORDER' | 'DELIVERY_ORDER',
|
||||||
|
'',
|
||||||
|
notes || `APPROVED marketing ${idsToProcess.join(', ')}`
|
||||||
|
)
|
||||||
|
: await SalesOrderApi.bulkApprovals(
|
||||||
|
idsToProcess,
|
||||||
|
approveAction,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(approveMarketingRes)) {
|
||||||
|
confirmationModal.closeModal();
|
||||||
|
toast.success(approveMarketingRes?.message as string);
|
||||||
|
setRowSelection({});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMarketing();
|
||||||
|
} finally {
|
||||||
|
setIsApproveLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkDeliveryDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
||||||
|
e
|
||||||
|
) => {
|
||||||
|
setBulkDeliveryDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkDeliveryNotesChangeHandler: ChangeEventHandler<
|
||||||
|
HTMLTextAreaElement
|
||||||
|
> = (e) => {
|
||||||
|
setBulkDeliveryNotes(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitBulkDeliveryApprovalHandler = async (
|
||||||
|
selectedIds: number[],
|
||||||
|
deliveryDate: string,
|
||||||
|
notes: string
|
||||||
|
) => {
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
toast.error('Tidak ada data yang valid untuk diproses.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deliveryDate) {
|
||||||
|
toast.error('Tanggal pengiriman wajib diisi.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmittingBulkDelivery(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bulkDeliveryApprovalRes = await MarketingApi.bulkApprovals(
|
||||||
|
selectedIds,
|
||||||
|
'DELIVERY_ORDER',
|
||||||
|
deliveryDate,
|
||||||
|
notes || `APPROVED delivery marketing ${selectedIds.join(', ')}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(bulkDeliveryApprovalRes)) {
|
||||||
|
toast.error(bulkDeliveryApprovalRes?.message as string);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isResponseSuccess(bulkDeliveryApprovalRes)) {
|
||||||
|
toast.error('Gagal memproses bulk approve delivery.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(bulkDeliveryApprovalRes?.message as string);
|
||||||
|
bulkDeliveryModal.closeModal();
|
||||||
|
setBulkDeliveryDate('');
|
||||||
|
setBulkDeliveryNotes('');
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
|
refreshMarketing();
|
||||||
|
} finally {
|
||||||
|
setIsSubmittingBulkDelivery(false);
|
||||||
}
|
}
|
||||||
if (isResponseError(approveMarketingRes)) {
|
|
||||||
confirmationModal.closeModal();
|
|
||||||
toast.error(approveMarketingRes?.message as string);
|
|
||||||
}
|
|
||||||
refreshMarketing();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||||
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
setIsDeliveryLoading(true);
|
||||||
deliveryModal.closeModal();
|
try {
|
||||||
toast.success(res?.message as string);
|
const res = await SalesOrderApi.delivery(
|
||||||
refreshMarketing?.();
|
selectedItem?.id as number,
|
||||||
router.push(
|
notes
|
||||||
`/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}`
|
);
|
||||||
);
|
deliveryModal.closeModal();
|
||||||
|
toast.success(res?.message as string);
|
||||||
|
refreshMarketing?.();
|
||||||
|
router.push(
|
||||||
|
`/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsDeliveryLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
const getRowCanSelect = useCallback(
|
||||||
const approval = row.original.latest_approval;
|
(row: Row<Marketing>): boolean => {
|
||||||
return approval?.step_number === 1 && approval?.action !== 'REJECTED';
|
const approval = row.original.latest_approval;
|
||||||
};
|
const isSelectableStep =
|
||||||
|
approval?.step_number === 1 || approval?.step_number === 2;
|
||||||
|
|
||||||
|
if (!isSelectableStep || approval?.action === 'REJECTED') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedApprovalStep === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return approval?.step_number === selectedApprovalStep;
|
||||||
|
},
|
||||||
|
[selectedApprovalStep]
|
||||||
|
);
|
||||||
|
|
||||||
const exportToExcelHandler = async () => {
|
const exportToExcelHandler = async () => {
|
||||||
setIsLoadingExportingToExcel(true);
|
setIsLoadingExportingToExcel(true);
|
||||||
@@ -329,6 +586,53 @@ const MarketingTable = () => {
|
|||||||
setIsLoadingExportingToExcel(false);
|
setIsLoadingExportingToExcel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetExportProgressForm = () => {
|
||||||
|
setExportProgressStartDate('');
|
||||||
|
setExportProgressEndDate('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportProgressStartDateChangeHandler: ChangeEventHandler<
|
||||||
|
HTMLInputElement
|
||||||
|
> = (e) => {
|
||||||
|
setExportProgressStartDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportProgressEndDateChangeHandler: ChangeEventHandler<
|
||||||
|
HTMLInputElement
|
||||||
|
> = (e) => {
|
||||||
|
setExportProgressEndDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportProgressInputToExcelClickHandler = () => {
|
||||||
|
resetExportProgressForm();
|
||||||
|
exportProgressInputModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitExportProgressInputHandler = async () => {
|
||||||
|
if (!exportProgressStartDate || !exportProgressEndDate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExportProgressLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await MarketingApi.exportInputProgressToExcel(
|
||||||
|
exportProgressStartDate,
|
||||||
|
exportProgressEndDate
|
||||||
|
);
|
||||||
|
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
toast.success('Ekspor berhasil');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsExportProgressLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -336,7 +640,22 @@ const MarketingTable = () => {
|
|||||||
size: 1,
|
size: 1,
|
||||||
header: ({ table }) => {
|
header: ({ table }) => {
|
||||||
const allRows = table.getRowModel().rows;
|
const allRows = table.getRowModel().rows;
|
||||||
const selectableRows = allRows.filter(getRowCanSelect);
|
const stepForBulkSelection =
|
||||||
|
selectedApprovalStep ??
|
||||||
|
allRows.find(getRowCanSelect)?.original.latest_approval.step_number;
|
||||||
|
const selectableRows = allRows.filter((row) => {
|
||||||
|
if (!getRowCanSelect(row)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stepForBulkSelection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
row.original.latest_approval.step_number === stepForBulkSelection
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const allSelected =
|
const allSelected =
|
||||||
selectableRows.length > 0 &&
|
selectableRows.length > 0 &&
|
||||||
@@ -378,7 +697,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'so_do_number',
|
accessorKey: 'so_number',
|
||||||
header: 'No. Order',
|
header: 'No. Order',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
return props.row.original.do_number
|
return props.row.original.do_number
|
||||||
@@ -394,7 +713,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'approval.step_name',
|
accessorKey: 'status',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
@@ -429,10 +748,12 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'customer.name',
|
accessorKey: 'customer',
|
||||||
header: 'Customer',
|
header: 'Customer',
|
||||||
|
cell: (props) => props.row.original.customer.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'grand_total',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.sales_order
|
row.sales_order
|
||||||
?.map((product) => product.total_price)
|
?.map((product) => product.total_price)
|
||||||
@@ -449,6 +770,7 @@ const MarketingTable = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'marketing_products.length',
|
accessorKey: 'marketing_products.length',
|
||||||
header: 'Product Details',
|
header: 'Product Details',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
if (props?.row?.original?.sales_order?.length) {
|
if (props?.row?.original?.sales_order?.length) {
|
||||||
if (props?.row?.original?.sales_order?.length > 1) {
|
if (props?.row?.original?.sales_order?.length > 1) {
|
||||||
@@ -470,6 +792,14 @@ const MarketingTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Tanggal Dibuat',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.created_at
|
||||||
|
? formatDate(props.row.original.created_at, 'DD MMM yyyy')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
maxSize: 80,
|
maxSize: 80,
|
||||||
@@ -504,7 +834,13 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, []);
|
}, [
|
||||||
|
deleteModal,
|
||||||
|
deliveryModal,
|
||||||
|
getRowCanSelect,
|
||||||
|
productsClickHandler,
|
||||||
|
selectedApprovalStep,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -527,7 +863,7 @@ const MarketingTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
{idsToProcess.length > 0 && (
|
{idsToProcess.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className='divider divider-horizontal w-px p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-px after:bg-base-content/10 after:w-px'></div>
|
<div className='divider divider-horizontal w-px p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-px after:bg-base-content/10 after:w-px' />
|
||||||
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
<Button
|
<Button
|
||||||
color='error'
|
color='error'
|
||||||
@@ -541,7 +877,7 @@ const MarketingTable = () => {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Reject
|
Reject ({idsToProcess.length} Item)
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
@@ -557,7 +893,7 @@ const MarketingTable = () => {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Approve
|
Approve ({idsToProcess.length} Item)
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</>
|
</>
|
||||||
@@ -566,7 +902,18 @@ const MarketingTable = () => {
|
|||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={['page', 'pageSize', 'search']}
|
excludeFields={[
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'search',
|
||||||
|
'product_names',
|
||||||
|
'status_name',
|
||||||
|
'customer_name',
|
||||||
|
'project_flock_name',
|
||||||
|
'project_flock_kandang_name',
|
||||||
|
'sort_by',
|
||||||
|
'order_by',
|
||||||
|
]}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
}}
|
}}
|
||||||
@@ -612,7 +959,17 @@ const MarketingTable = () => {
|
|||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
Export to Excel
|
Ekspor ke Excel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportProgressInputToExcelClickHandler}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Ekspor Input Progress (Excel)
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
@@ -646,6 +1003,9 @@ const MarketingTable = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
pageSize={tableFilterState.pageSize}
|
pageSize={tableFilterState.pageSize}
|
||||||
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
totalItems={
|
totalItems={
|
||||||
isResponseSuccess(marketing)
|
isResponseSuccess(marketing)
|
||||||
? marketing?.meta?.total_results
|
? marketing?.meta?.total_results
|
||||||
@@ -677,14 +1037,16 @@ const MarketingTable = () => {
|
|||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={confirmationModal.ref}
|
ref={confirmationModal.ref}
|
||||||
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
||||||
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan (${idsToProcess.length} data)?`}
|
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan tahap ${selectedApprovalStep ?? '-'} (${idsToProcess.length} data)?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
|
isLoading: isApproveLoading,
|
||||||
onClick: confirmationModal.closeModal,
|
onClick: confirmationModal.closeModal,
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
||||||
|
isLoading: isApproveLoading,
|
||||||
onClick: approveMarketingHandler,
|
onClick: approveMarketingHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -708,14 +1070,169 @@ const MarketingTable = () => {
|
|||||||
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
|
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
|
isLoading: isDeliveryLoading,
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
|
isLoading: isDeliveryLoading,
|
||||||
onClick: confirmationModalDeliveryClickHandler,
|
onClick: confirmationModalDeliveryClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
ref={bulkDeliveryModal.ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'max-w-lg rounded-lg p-0',
|
||||||
|
}}
|
||||||
|
closeOnBackdrop
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
|
Bulk Approve Delivery
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
bulkDeliveryModal.closeModal();
|
||||||
|
setBulkDeliveryDate('');
|
||||||
|
setBulkDeliveryNotes('');
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<p className='text-sm text-base-content/70'>
|
||||||
|
Pilih tanggal pengiriman untuk approve {idsToProcess.length} data
|
||||||
|
penjualan tahap 2.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='bulk_delivery_date'
|
||||||
|
label='Tanggal Pengiriman'
|
||||||
|
value={bulkDeliveryDate}
|
||||||
|
onChange={bulkDeliveryDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
name='bulk_delivery_notes'
|
||||||
|
label='Catatan'
|
||||||
|
placeholder='Masukkan catatan approval...'
|
||||||
|
value={bulkDeliveryNotes}
|
||||||
|
onChange={bulkDeliveryNotesChangeHandler}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
disabled={isSubmittingBulkDelivery}
|
||||||
|
onClick={() => {
|
||||||
|
bulkDeliveryModal.closeModal();
|
||||||
|
setBulkDeliveryDate('');
|
||||||
|
setBulkDeliveryNotes('');
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='success'
|
||||||
|
isLoading={isSubmittingBulkDelivery}
|
||||||
|
disabled={isSubmittingBulkDelivery}
|
||||||
|
onClick={() =>
|
||||||
|
submitBulkDeliveryApprovalHandler(
|
||||||
|
idsToProcess,
|
||||||
|
bulkDeliveryDate,
|
||||||
|
bulkDeliveryNotes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
ref={exportProgressInputModal.ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'max-w-lg rounded-lg p-0',
|
||||||
|
}}
|
||||||
|
closeOnBackdrop
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
|
Ekspor Input Progress
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_start_date'
|
||||||
|
label='Tanggal Mulai'
|
||||||
|
value={exportProgressStartDate}
|
||||||
|
onChange={exportProgressStartDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_end_date'
|
||||||
|
label='Tanggal Selesai'
|
||||||
|
value={exportProgressEndDate}
|
||||||
|
onChange={exportProgressEndDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='success'
|
||||||
|
onClick={submitExportProgressInputHandler}
|
||||||
|
isLoading={isExportProgressLoading}
|
||||||
|
disabled={!exportProgressStartDate || !exportProgressEndDate}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
ref={productsModal.ref}
|
ref={productsModal.ref}
|
||||||
className={{
|
className={{
|
||||||
@@ -746,7 +1263,7 @@ const MarketingTable = () => {
|
|||||||
}
|
}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'Kandang',
|
header: 'Gudang Fisik',
|
||||||
accessorFn(row) {
|
accessorFn(row) {
|
||||||
return row.product_warehouse.warehouse.name;
|
return row.product_warehouse.warehouse.name;
|
||||||
},
|
},
|
||||||
@@ -777,6 +1294,7 @@ const MarketingTable = () => {
|
|||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
|
initialValues={marketingFilterInitialValues}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -195,7 +195,9 @@ const SalesOrderFormModal = ({
|
|||||||
product.marketing_type?.value?.toLowerCase() === 'telur'
|
product.marketing_type?.value?.toLowerCase() === 'telur'
|
||||||
? convertionUnitValue === 'PETI'
|
? convertionUnitValue === 'PETI'
|
||||||
? 'PETI'
|
? 'PETI'
|
||||||
: 'KG' // termasuk "QTY" dan "KG"
|
: convertionUnitValue === 'QTY'
|
||||||
|
? 'QTY'
|
||||||
|
: 'KG'
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM"
|
// Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM"
|
||||||
@@ -207,7 +209,6 @@ const SalesOrderFormModal = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
vehicle_number: product.vehicle_number as string,
|
vehicle_number: product.vehicle_number as string,
|
||||||
kandang_id: product.kandang_id as number,
|
|
||||||
product_warehouse_id: product.product_warehouse_id as number,
|
product_warehouse_id: product.product_warehouse_id as number,
|
||||||
unit_price: parseFloat(String(product.unit_price || 0)),
|
unit_price: parseFloat(String(product.unit_price || 0)),
|
||||||
total_weight: parseFloat(String(product.total_weight || 0)),
|
total_weight: parseFloat(String(product.total_weight || 0)),
|
||||||
@@ -245,6 +246,7 @@ const SalesOrderFormModal = ({
|
|||||||
})
|
})
|
||||||
.filter((item) => Boolean(item)),
|
.filter((item) => Boolean(item)),
|
||||||
} as UpdateDeliveryOrderPayload);
|
} as UpdateDeliveryOrderPayload);
|
||||||
|
|
||||||
switch (modalAction) {
|
switch (modalAction) {
|
||||||
case 'add':
|
case 'add':
|
||||||
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
||||||
@@ -260,11 +262,7 @@ const SalesOrderFormModal = ({
|
|||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
||||||
useFormikErrorList(formik, {
|
useFormikErrorList(formik);
|
||||||
onAfterSubmit: () => {
|
|
||||||
router.push('/marketing');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ================== FORM REPEATER HANDLER ==================
|
// ================== FORM REPEATER HANDLER ==================
|
||||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { array, mixed, object } from 'yup';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
|
||||||
|
export const MarketingFilterSchema = object({
|
||||||
|
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
|
||||||
|
status: mixed<OptionType<string>>().nullable(),
|
||||||
|
customer: mixed<OptionType<number>>().nullable(),
|
||||||
|
project_flock: mixed<OptionType<number>>().nullable(),
|
||||||
|
project_flock_kandang: mixed<OptionType<number>>().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type MarketingFilterFormValues = {
|
||||||
|
product_ids: OptionType<number>[];
|
||||||
|
status: OptionType<string> | null;
|
||||||
|
customer: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Marketing,
|
Marketing,
|
||||||
} from '@/types/api/marketing/marketing';
|
} from '@/types/api/marketing/marketing';
|
||||||
import { formatDate, formatTitleCase } from '@/lib/helper';
|
import { formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
|
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
|
||||||
|
|
||||||
type MarketingSchemaType = {
|
type MarketingSchemaType = {
|
||||||
customer_id: number | undefined;
|
customer_id: number | undefined;
|
||||||
@@ -70,14 +71,14 @@ export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
|
|||||||
.required('Pengiriman wajib diisi!')
|
.required('Pengiriman wajib diisi!')
|
||||||
.test(
|
.test(
|
||||||
'at-least-one-valid-row',
|
'at-least-one-valid-row',
|
||||||
'Minimal harus ada satu baris pengiriman yang lengkap diisi!',
|
'Seluruh data pengiriman harus diisi lengkap!',
|
||||||
function (items) {
|
function (items) {
|
||||||
if (!items || items.length === 0) return false;
|
if (!items || items.length === 0) return false;
|
||||||
|
|
||||||
// VALIDASI: minimal 1 item valid full
|
// VALIDASI: seluruh item harus valid full
|
||||||
const itemSchema = DeliveryOrderProductSchema;
|
const itemSchema = DeliveryOrderProductSchema;
|
||||||
|
|
||||||
const hasValidItem = items.some((item) => {
|
const hasValidItem = items.every((item) => {
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
return itemSchema.isValidSync(item, { abortEarly: true });
|
return itemSchema.isValidSync(item, { abortEarly: true });
|
||||||
});
|
});
|
||||||
@@ -97,17 +98,21 @@ export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
|
|||||||
export const SalesProductToFieldValues = (
|
export const SalesProductToFieldValues = (
|
||||||
product: BaseSalesOrder
|
product: BaseSalesOrder
|
||||||
): SalesOrderProductFormValues => {
|
): SalesOrderProductFormValues => {
|
||||||
|
const warehouseOption = {
|
||||||
|
value: product.product_warehouse.warehouse.id,
|
||||||
|
label: product.product_warehouse.warehouse.name,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
vehicle_number: product.vehicle_number,
|
vehicle_number: product.vehicle_number,
|
||||||
|
warehouse_id: product.product_warehouse.warehouse.id,
|
||||||
|
warehouse: warehouseOption,
|
||||||
kandang_id: product.product_warehouse.warehouse.id,
|
kandang_id: product.product_warehouse.warehouse.id,
|
||||||
kandang: {
|
kandang: warehouseOption,
|
||||||
value: product.product_warehouse.warehouse.id,
|
|
||||||
label: product.product_warehouse.warehouse.name,
|
|
||||||
},
|
|
||||||
product_warehouse: {
|
product_warehouse: {
|
||||||
value: product.product_warehouse.id,
|
value: product.product_warehouse.id,
|
||||||
label: product.product_warehouse.product.name,
|
label: getProductWarehouseOptionLabel(product.product_warehouse),
|
||||||
},
|
},
|
||||||
product_warehouse_data: product.product_warehouse,
|
product_warehouse_data: product.product_warehouse,
|
||||||
product_warehouse_id: product.product_warehouse.id,
|
product_warehouse_id: product.product_warehouse.id,
|
||||||
@@ -118,8 +123,17 @@ export const SalesProductToFieldValues = (
|
|||||||
total_price: product.total_price,
|
total_price: product.total_price,
|
||||||
marketing_type: product.marketing_type
|
marketing_type: product.marketing_type
|
||||||
? {
|
? {
|
||||||
value: product.marketing_type,
|
value:
|
||||||
label: formatTitleCase(product.marketing_type),
|
product.marketing_type === 'AYAM' ||
|
||||||
|
product.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM,AYAM_PULLET'
|
||||||
|
: product.marketing_type,
|
||||||
|
label: formatTitleCase(
|
||||||
|
product.marketing_type === 'AYAM' ||
|
||||||
|
product.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM'
|
||||||
|
: product.marketing_type
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
convertion_unit: product.convertion_unit
|
convertion_unit: product.convertion_unit
|
||||||
@@ -139,11 +153,36 @@ export const DeliveryProductToFieldValues = (
|
|||||||
delivery: BaseDeliveryOrder
|
delivery: BaseDeliveryOrder
|
||||||
): DeliveryOrderProductFormValues[] => {
|
): DeliveryOrderProductFormValues[] => {
|
||||||
const data = delivery.deliveries.map((item) => {
|
const data = delivery.deliveries.map((item) => {
|
||||||
const soId = salesOrders.find(
|
const salesOrder =
|
||||||
(so) => so.product_warehouse.id === item.product_warehouse.id
|
salesOrders.find((so) => so.id === item.marketing_product_id) ??
|
||||||
)?.id;
|
salesOrders.find(
|
||||||
|
(so) => so.product_warehouse.id === item.product_warehouse.id
|
||||||
|
);
|
||||||
|
const warehouseOption = {
|
||||||
|
value: item.product_warehouse.warehouse.id,
|
||||||
|
label: item.product_warehouse.warehouse.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialSisaBerat =
|
||||||
|
item?.total_weight &&
|
||||||
|
salesOrder?.weight_per_convertion &&
|
||||||
|
salesOrder?.total_peti
|
||||||
|
? Number(item.total_weight) -
|
||||||
|
Number(salesOrder.weight_per_convertion) *
|
||||||
|
Number(salesOrder.total_peti)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const initialPricePerConvertion =
|
||||||
|
item?.total_price &&
|
||||||
|
salesOrder?.total_peti &&
|
||||||
|
Number(salesOrder.total_peti) !== 0
|
||||||
|
? (Number(item.total_price) -
|
||||||
|
initialSisaBerat * Number(item.unit_price || 0)) /
|
||||||
|
Number(salesOrder.total_peti)
|
||||||
|
: Number(item?.unit_price || 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: soId,
|
id: salesOrder?.id,
|
||||||
unit_price: item.unit_price,
|
unit_price: item.unit_price,
|
||||||
total_weight: item.total_weight,
|
total_weight: item.total_weight,
|
||||||
qty: item.qty,
|
qty: item.qty,
|
||||||
@@ -152,19 +191,40 @@ export const DeliveryProductToFieldValues = (
|
|||||||
vehicle_number: item.vehicle_number,
|
vehicle_number: item.vehicle_number,
|
||||||
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
|
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
|
||||||
do_number: delivery.do_number,
|
do_number: delivery.do_number,
|
||||||
marketing_product_id: soId,
|
marketing_product_id: item.marketing_product_id ?? salesOrder?.id,
|
||||||
|
marketing_type: salesOrder?.marketing_type
|
||||||
|
? {
|
||||||
|
value:
|
||||||
|
salesOrder?.marketing_type === 'AYAM' ||
|
||||||
|
salesOrder?.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM,AYAM_PULLET'
|
||||||
|
: salesOrder?.marketing_type,
|
||||||
|
label: formatTitleCase(
|
||||||
|
salesOrder?.marketing_type === 'AYAM' ||
|
||||||
|
salesOrder?.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM'
|
||||||
|
: salesOrder?.marketing_type
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
convertion_unit: salesOrder?.convertion_unit
|
||||||
|
? {
|
||||||
|
value: salesOrder?.convertion_unit.toLowerCase(),
|
||||||
|
label: formatTitleCase(salesOrder?.convertion_unit),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
marketing_product: {
|
marketing_product: {
|
||||||
id: soId,
|
id: item.marketing_product_id ?? salesOrder?.id,
|
||||||
vehicle_number: item.vehicle_number,
|
vehicle_number: item.vehicle_number,
|
||||||
|
warehouse_id: item.product_warehouse.warehouse.id,
|
||||||
|
warehouse: warehouseOption,
|
||||||
kandang_id: item.product_warehouse.warehouse.id,
|
kandang_id: item.product_warehouse.warehouse.id,
|
||||||
kandang: {
|
kandang: warehouseOption,
|
||||||
value: item.product_warehouse.warehouse.id,
|
|
||||||
label: item.product_warehouse.warehouse.name,
|
|
||||||
},
|
|
||||||
product_warehouse: {
|
product_warehouse: {
|
||||||
value: item.product_warehouse.id,
|
value: item.product_warehouse.id,
|
||||||
label: item.product_warehouse.product.name,
|
label: getProductWarehouseOptionLabel(item.product_warehouse),
|
||||||
},
|
},
|
||||||
|
product_warehouse_data: item.product_warehouse,
|
||||||
product_warehouse_id: item.product_warehouse.id,
|
product_warehouse_id: item.product_warehouse.id,
|
||||||
unit_price: item.unit_price,
|
unit_price: item.unit_price,
|
||||||
total_weight: item.total_weight,
|
total_weight: item.total_weight,
|
||||||
@@ -172,8 +232,13 @@ export const DeliveryProductToFieldValues = (
|
|||||||
avg_weight: item.avg_weight,
|
avg_weight: item.avg_weight,
|
||||||
total_price: item.total_price,
|
total_price: item.total_price,
|
||||||
},
|
},
|
||||||
|
total_peti: salesOrder?.total_peti,
|
||||||
|
weight_per_convertion:
|
||||||
|
item?.weight_per_convertion ?? salesOrder?.weight_per_convertion ?? 0,
|
||||||
|
price_per_convertion: initialPricePerConvertion,
|
||||||
} as DeliveryOrderProductFormValues;
|
} as DeliveryOrderProductFormValues;
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
export const mergeSOwithDO = (
|
export const mergeSOwithDO = (
|
||||||
@@ -181,10 +246,25 @@ export const mergeSOwithDO = (
|
|||||||
deliveryOrders: DeliveryOrderProductFormValues[],
|
deliveryOrders: DeliveryOrderProductFormValues[],
|
||||||
autofill?: boolean
|
autofill?: boolean
|
||||||
): DeliveryOrderProductFormValues[] => {
|
): DeliveryOrderProductFormValues[] => {
|
||||||
|
const hasDeliveryOrders = deliveryOrders.length > 0;
|
||||||
|
|
||||||
return salesOrders.map((so) => {
|
return salesOrders.map((so) => {
|
||||||
const delivery = deliveryOrders.find(
|
const delivery = deliveryOrders.find(
|
||||||
(d) => d?.marketing_product_id === so.id
|
(d) => d?.marketing_product_id === so.id
|
||||||
);
|
);
|
||||||
|
const isTelurQty =
|
||||||
|
so.marketing_type?.value?.toLowerCase() === 'telur' &&
|
||||||
|
so.convertion_unit?.value?.toLowerCase() === 'qty';
|
||||||
|
const salesOrderUnitPrice =
|
||||||
|
isTelurQty && Number(so.total_price || 0) > 0 && Number(so.qty || 0) > 0
|
||||||
|
? Number(so.total_price) / Number(so.qty)
|
||||||
|
: so.unit_price;
|
||||||
|
const salesOrderPricePerQty =
|
||||||
|
isTelurQty &&
|
||||||
|
Number(so.total_price || 0) > 0 &&
|
||||||
|
Number(so.total_weight || 0) > 0
|
||||||
|
? Number(so.total_price) / Number(so.total_weight)
|
||||||
|
: so.price_per_qty;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...so, // nilai dasar dari sales order
|
...so, // nilai dasar dari sales order
|
||||||
@@ -192,30 +272,50 @@ export const mergeSOwithDO = (
|
|||||||
delivery_date: delivery?.delivery_date || undefined,
|
delivery_date: delivery?.delivery_date || undefined,
|
||||||
do_number: delivery?.do_number || undefined,
|
do_number: delivery?.do_number || undefined,
|
||||||
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
|
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
|
||||||
unit_price: autofill ? so.unit_price : delivery?.unit_price,
|
unit_price:
|
||||||
total_weight: autofill ? so.total_weight : delivery?.total_weight,
|
autofill && hasDeliveryOrders
|
||||||
qty: autofill ? so.qty : delivery?.qty,
|
? delivery?.unit_price
|
||||||
avg_weight: autofill ? so.avg_weight : delivery?.avg_weight,
|
: salesOrderUnitPrice,
|
||||||
total_price: autofill ? so.total_price : delivery?.total_price,
|
total_weight:
|
||||||
|
autofill && hasDeliveryOrders
|
||||||
|
? delivery?.total_weight
|
||||||
|
: so.total_weight,
|
||||||
|
qty: autofill && hasDeliveryOrders ? delivery?.qty : so.qty,
|
||||||
|
avg_weight:
|
||||||
|
autofill && hasDeliveryOrders ? delivery?.avg_weight : so.avg_weight,
|
||||||
|
total_price:
|
||||||
|
autofill && hasDeliveryOrders ? delivery?.total_price : so.total_price,
|
||||||
marketing_product: so, // jika ada, override
|
marketing_product: so, // jika ada, override
|
||||||
uom: autofill ? so.uom : delivery?.uom,
|
uom: autofill && hasDeliveryOrders ? delivery?.uom : so.uom,
|
||||||
weight_per_convertion: autofill
|
weight_per_convertion:
|
||||||
? so.weight_per_convertion
|
autofill && hasDeliveryOrders
|
||||||
: delivery?.weight_per_convertion,
|
? delivery?.weight_per_convertion
|
||||||
price_per_convertion: autofill
|
: so.weight_per_convertion,
|
||||||
? so.price_per_convertion
|
price_per_convertion:
|
||||||
: delivery?.price_per_convertion,
|
autofill && hasDeliveryOrders
|
||||||
convertion_unit: autofill
|
? delivery?.price_per_convertion
|
||||||
? so.convertion_unit
|
: so.price_per_convertion,
|
||||||
: delivery?.convertion_unit,
|
convertion_unit:
|
||||||
marketing_type: autofill ? so.marketing_type : delivery?.marketing_type,
|
autofill && hasDeliveryOrders
|
||||||
total_peti: autofill ? so.total_peti : delivery?.total_peti,
|
? delivery?.convertion_unit
|
||||||
price_per_qty: autofill ? so.price_per_qty : delivery?.price_per_qty,
|
: so.convertion_unit,
|
||||||
sisa_berat: autofill ? so.sisa_berat : delivery?.sisa_berat,
|
marketing_type:
|
||||||
price_sisa_berat: autofill
|
autofill && hasDeliveryOrders
|
||||||
? so.price_sisa_berat
|
? delivery?.marketing_type
|
||||||
: delivery?.price_sisa_berat,
|
: so.marketing_type,
|
||||||
week: autofill ? so.week : delivery?.week,
|
total_peti:
|
||||||
|
autofill && hasDeliveryOrders ? delivery?.total_peti : so.total_peti,
|
||||||
|
price_per_qty:
|
||||||
|
autofill && hasDeliveryOrders
|
||||||
|
? delivery?.price_per_qty
|
||||||
|
: salesOrderPricePerQty,
|
||||||
|
sisa_berat:
|
||||||
|
autofill && hasDeliveryOrders ? delivery?.sisa_berat : so.sisa_berat,
|
||||||
|
price_sisa_berat:
|
||||||
|
autofill && hasDeliveryOrders
|
||||||
|
? delivery?.price_sisa_berat
|
||||||
|
: so.price_sisa_berat,
|
||||||
|
week: autofill && hasDeliveryOrders ? delivery?.week : so.week,
|
||||||
} as DeliveryOrderProductFormValues;
|
} as DeliveryOrderProductFormValues;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+145
-56
@@ -32,6 +32,63 @@ import Dropdown from '@/components/Dropdown';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
|
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
|
||||||
|
|
||||||
|
type PricingOption =
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
type PricingSource =
|
||||||
|
| {
|
||||||
|
marketing_type?: PricingOption;
|
||||||
|
convertion_unit?: PricingOption;
|
||||||
|
total_price?: string | number | null;
|
||||||
|
qty?: string | number | null;
|
||||||
|
total_weight?: string | number | null;
|
||||||
|
unit_price?: string | number | null;
|
||||||
|
price_per_qty?: number | null;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const getOptionValue = (value?: PricingOption) => {
|
||||||
|
if (!value) return undefined;
|
||||||
|
if (typeof value === 'string') return value.toLowerCase();
|
||||||
|
|
||||||
|
return value.value?.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTelurQtyProduct = (value?: PricingSource) =>
|
||||||
|
getOptionValue(value?.marketing_type) === 'telur' &&
|
||||||
|
getOptionValue(value?.convertion_unit) === 'qty';
|
||||||
|
|
||||||
|
const getDisplayedUnitPrice = (value?: PricingSource) => {
|
||||||
|
if (
|
||||||
|
isTelurQtyProduct(value) &&
|
||||||
|
Number(value?.total_price || 0) > 0 &&
|
||||||
|
Number(value?.qty || 0) > 0
|
||||||
|
) {
|
||||||
|
return Number(value?.total_price) / Number(value?.qty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value?.unit_price ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayedPricePerQty = (value?: PricingSource) => {
|
||||||
|
if (
|
||||||
|
isTelurQtyProduct(value) &&
|
||||||
|
Number(value?.total_price || 0) > 0 &&
|
||||||
|
Number(value?.total_weight || 0) > 0
|
||||||
|
) {
|
||||||
|
return Number(value?.total_price) / Number(value?.total_weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value?.price_per_qty ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
const DeliveryOrderProductForm = ({
|
const DeliveryOrderProductForm = ({
|
||||||
formState,
|
formState,
|
||||||
salesOrders,
|
salesOrders,
|
||||||
@@ -76,7 +133,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
? (Number(initialValues.total_price) -
|
? (Number(initialValues.total_price) -
|
||||||
initialSisaBerat * Number(initialValues.unit_price || 0)) /
|
initialSisaBerat * Number(initialValues.unit_price || 0)) /
|
||||||
Number(initialValues.total_peti)
|
Number(initialValues.total_peti)
|
||||||
: 0;
|
: Number(initialValues?.unit_price || 0);
|
||||||
|
|
||||||
const initialPriceSisaBerat =
|
const initialPriceSisaBerat =
|
||||||
initialValues?.total_price && initialValues?.total_peti
|
initialValues?.total_price && initialValues?.total_peti
|
||||||
@@ -89,15 +146,6 @@ const DeliveryOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ============ Fetch Data ============
|
// ============ Fetch Data ============
|
||||||
const { data: productData } = useSWR(
|
|
||||||
selectedProduct?.value
|
|
||||||
? ProductApi.basePath + '/' + selectedProduct?.value
|
|
||||||
: null,
|
|
||||||
() =>
|
|
||||||
selectedProduct?.value
|
|
||||||
? ProductApi.getSingle(Number(selectedProduct?.value))
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
// Options Week dari minggu 1 - 22
|
// Options Week dari minggu 1 - 22
|
||||||
// const optionsWeek = useMemo(() => {
|
// const optionsWeek = useMemo(() => {
|
||||||
@@ -112,7 +160,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
if (!Boolean(item.qty)) {
|
if (!Boolean(item.qty)) {
|
||||||
return {
|
return {
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`,
|
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.warehouse?.label ?? item.marketing_product?.kandang?.label}`,
|
||||||
} as OptionType;
|
} as OptionType;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@@ -133,12 +181,19 @@ const DeliveryOrderProductForm = ({
|
|||||||
const deliveryOrder = useMemo(() => {
|
const deliveryOrder = useMemo(() => {
|
||||||
if (!hasDeliveryOrder || !deliveryOrders) return null;
|
if (!hasDeliveryOrder || !deliveryOrders) return null;
|
||||||
|
|
||||||
|
const marketingProductId =
|
||||||
|
initialValues?.marketing_product_id ?? initialValues?.id;
|
||||||
|
|
||||||
for (const doItem of deliveryOrders) {
|
for (const doItem of deliveryOrders) {
|
||||||
const found = doItem.deliveries.find(
|
const found =
|
||||||
(d) =>
|
doItem.deliveries.find(
|
||||||
d.product_warehouse.id ===
|
(d) => d.marketing_product_id === marketingProductId
|
||||||
initialValues?.marketing_product?.product_warehouse_id
|
) ??
|
||||||
);
|
doItem.deliveries.find(
|
||||||
|
(d) =>
|
||||||
|
d.product_warehouse.id ===
|
||||||
|
initialValues?.marketing_product?.product_warehouse_id
|
||||||
|
);
|
||||||
if (found) {
|
if (found) {
|
||||||
return {
|
return {
|
||||||
...found,
|
...found,
|
||||||
@@ -154,6 +209,27 @@ const DeliveryOrderProductForm = ({
|
|||||||
(item) => item.id === initialValues?.marketing_product_id
|
(item) => item.id === initialValues?.marketing_product_id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const defaultPricingSource: PricingSource = {
|
||||||
|
marketing_type:
|
||||||
|
initialValues?.marketing_type ?? salesOrder?.marketing_type ?? null,
|
||||||
|
convertion_unit:
|
||||||
|
initialValues?.convertion_unit ?? salesOrder?.convertion_unit ?? null,
|
||||||
|
total_price:
|
||||||
|
deliveryOrder?.total_price ??
|
||||||
|
initialValues?.total_price ??
|
||||||
|
salesOrder?.total_price,
|
||||||
|
qty: deliveryOrder?.qty ?? initialValues?.qty ?? salesOrder?.qty,
|
||||||
|
total_weight:
|
||||||
|
deliveryOrder?.total_weight ??
|
||||||
|
initialValues?.total_weight ??
|
||||||
|
salesOrder?.total_weight,
|
||||||
|
unit_price:
|
||||||
|
deliveryOrder?.unit_price ??
|
||||||
|
initialValues?.unit_price ??
|
||||||
|
salesOrder?.unit_price,
|
||||||
|
price_per_qty: initialValues?.price_per_qty ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
const formik = useFormik<DeliveryOrderProductFormValues>({
|
const formik = useFormik<DeliveryOrderProductFormValues>({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -167,8 +243,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
undefined,
|
undefined,
|
||||||
marketing_product_id:
|
marketing_product_id:
|
||||||
salesOrder?.id || initialValues?.marketing_product_id || undefined,
|
salesOrder?.id || initialValues?.marketing_product_id || undefined,
|
||||||
unit_price:
|
unit_price: getDisplayedUnitPrice(defaultPricingSource),
|
||||||
deliveryOrder?.unit_price ?? initialValues?.unit_price ?? undefined,
|
|
||||||
total_weight:
|
total_weight:
|
||||||
deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined,
|
deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined,
|
||||||
qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined,
|
qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined,
|
||||||
@@ -186,7 +261,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
convertion_unit: initialValues?.convertion_unit || null,
|
convertion_unit: initialValues?.convertion_unit || null,
|
||||||
marketing_type: initialValues?.marketing_type || null,
|
marketing_type: initialValues?.marketing_type || null,
|
||||||
total_peti: initialValues?.total_peti ?? null,
|
total_peti: initialValues?.total_peti ?? null,
|
||||||
price_per_qty: initialValues?.price_per_qty ?? null,
|
price_per_qty: getDisplayedPricePerQty(defaultPricingSource),
|
||||||
sisa_berat: initialSisaBerat,
|
sisa_berat: initialSisaBerat,
|
||||||
price_sisa_berat: initialPriceSisaBerat,
|
price_sisa_berat: initialPriceSisaBerat,
|
||||||
week: initialValues?.week ?? null,
|
week: initialValues?.week ?? null,
|
||||||
@@ -326,14 +401,21 @@ const DeliveryOrderProductForm = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues) {
|
if (initialValues) {
|
||||||
if (!Boolean(initialValues.qty)) {
|
if (
|
||||||
|
!Boolean(initialValues.qty) &&
|
||||||
|
!Boolean(initialValues.marketing_product_id)
|
||||||
|
) {
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
} else {
|
} else {
|
||||||
setFormikValues(initialValues);
|
setFormikValues({
|
||||||
|
...initialValues,
|
||||||
|
unit_price: getDisplayedUnitPrice(initialValues),
|
||||||
|
price_per_qty: getDisplayedPricePerQty(initialValues),
|
||||||
|
});
|
||||||
if (initialValues?.marketing_product_id) {
|
if (initialValues?.marketing_product_id) {
|
||||||
setSelectedProduct({
|
setSelectedProduct({
|
||||||
value: initialValues?.id,
|
value: initialValues?.marketing_product_id,
|
||||||
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.kandang?.label}`,
|
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.warehouse?.label ?? initialValues?.marketing_product?.kandang?.label}`,
|
||||||
} as OptionType);
|
} as OptionType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,7 +431,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
handleBlurField(currentInput);
|
handleBlurField(currentInput);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'uom',
|
'uom',
|
||||||
isResponseSuccess(productData) ? productData?.data?.uom?.name : ''
|
initialValues?.marketing_product?.product_warehouse_data?.product?.uom
|
||||||
|
?.name ?? ''
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -458,10 +541,11 @@ const DeliveryOrderProductForm = ({
|
|||||||
marketing_product_id: selected.value as number,
|
marketing_product_id: selected.value as number,
|
||||||
marketing_product: soFieldValues,
|
marketing_product: soFieldValues,
|
||||||
qty: so.qty,
|
qty: so.qty,
|
||||||
unit_price: so.unit_price,
|
unit_price: getDisplayedUnitPrice(so),
|
||||||
total_price: so.total_price,
|
total_price: so.total_price,
|
||||||
avg_weight: so.avg_weight,
|
avg_weight: so.avg_weight,
|
||||||
total_weight: so.total_weight,
|
total_weight: so.total_weight,
|
||||||
|
price_per_qty: getDisplayedPricePerQty(so),
|
||||||
vehicle_number: so.vehicle_number,
|
vehicle_number: so.vehicle_number,
|
||||||
week: soFieldValues.week ?? null,
|
week: soFieldValues.week ?? null,
|
||||||
});
|
});
|
||||||
@@ -472,7 +556,11 @@ const DeliveryOrderProductForm = ({
|
|||||||
text={
|
text={
|
||||||
exisitingValues?.find(
|
exisitingValues?.find(
|
||||||
(item) => item.id === selectedProduct?.value
|
(item) => item.id === selectedProduct?.value
|
||||||
)?.marketing_product?.kandang?.label ?? ''
|
)?.marketing_product?.warehouse?.label ??
|
||||||
|
exisitingValues?.find(
|
||||||
|
(item) => item.id === selectedProduct?.value
|
||||||
|
)?.marketing_product?.kandang?.label ??
|
||||||
|
''
|
||||||
}
|
}
|
||||||
color='success'
|
color='success'
|
||||||
className={{
|
className={{
|
||||||
@@ -638,7 +726,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
placeholder='Masukan Total Peti'
|
placeholder='Masukan Total Peti'
|
||||||
endAdornment={
|
endAdornment={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span className='text-sm text-base-content/50'>Kg</span>
|
<span className='text-sm text-base-content/50'>Peti</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
||||||
@@ -688,6 +776,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
errorMessage={formik.errors.total_weight}
|
errorMessage={formik.errors.total_weight}
|
||||||
placeholder='Masukan Total Bobot'
|
placeholder='Masukan Total Bobot'
|
||||||
|
disabled={
|
||||||
|
formik.values.convertion_unit?.value.toLowerCase() === 'peti'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -714,9 +805,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
endAdornment={
|
endAdornment={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span className='text-sm text-gray-500'>
|
<span className='text-sm text-gray-500'>
|
||||||
{isResponseSuccess(productData)
|
{initialValues?.marketing_product?.product_warehouse_data
|
||||||
? productData?.data?.uom.name
|
?.product?.uom?.name ?? ''}
|
||||||
: ''}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -727,9 +817,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
(item) => item.id === formik.values.marketing_product_id
|
(item) => item.id === formik.values.marketing_product_id
|
||||||
)?.qty +
|
)?.qty +
|
||||||
' ' +
|
' ' +
|
||||||
(isResponseSuccess(productData)
|
(initialValues?.marketing_product?.product_warehouse_data
|
||||||
? productData?.data?.uom.name
|
?.product?.uom?.name ?? '')
|
||||||
: '')
|
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -757,12 +846,32 @@ const DeliveryOrderProductForm = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Harga per butir untuk TELUR + QTY */}
|
{/* Harga Satuan */}
|
||||||
|
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
||||||
|
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
label={`Harga / ${formik.values.convertion_unit?.label.toLowerCase() !== 'qty' ? 'Kg' : 'Butir'} (Rp)`}
|
||||||
|
name='unit_price'
|
||||||
|
value={formik.values.unit_price}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
handleFieldChange('unit_price', value, () =>
|
||||||
|
setCurrentInput(e.target.name)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isError={Boolean(formik.errors.unit_price)}
|
||||||
|
errorMessage={formik.errors.unit_price}
|
||||||
|
placeholder='Masukan Harga Satuan'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Harga per kg untuk TELUR + QTY */}
|
||||||
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
|
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
|
||||||
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
|
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga / Butir (Rp)'
|
label='Harga / Kg (Rp)'
|
||||||
name='price_per_qty'
|
name='price_per_qty'
|
||||||
value={formik.values.price_per_qty ?? undefined}
|
value={formik.values.price_per_qty ?? undefined}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -776,27 +885,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
Boolean(formik.errors.price_per_qty)
|
Boolean(formik.errors.price_per_qty)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.price_per_qty}
|
errorMessage={formik.errors.price_per_qty}
|
||||||
placeholder='Masukan Harga per Butir'
|
placeholder='Masukan Harga per Kg'
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Harga Satuan */}
|
|
||||||
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
|
||||||
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
|
||||||
<NumberInput
|
|
||||||
required
|
|
||||||
label={`Harga / ${isResponseSuccess(productData) ? productData?.data?.uom?.name : 'Produk'} (Rp)`}
|
|
||||||
name='unit_price'
|
|
||||||
value={formik.values.unit_price}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = Number(e.target.value);
|
|
||||||
handleFieldChange('unit_price', value, () =>
|
|
||||||
setCurrentInput(e.target.name)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
isError={Boolean(formik.errors.unit_price)}
|
|
||||||
errorMessage={formik.errors.unit_price}
|
|
||||||
placeholder='Masukan Harga Satuan'
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+19
-7
@@ -3,6 +3,11 @@ import * as Yup from 'yup';
|
|||||||
|
|
||||||
type SalesOrderProductSchemaType = {
|
type SalesOrderProductSchemaType = {
|
||||||
id?: number | undefined;
|
id?: number | undefined;
|
||||||
|
warehouse_id?: number;
|
||||||
|
warehouse?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
kandang_id?: number;
|
kandang_id?: number;
|
||||||
kandang?: {
|
kandang?: {
|
||||||
value: number;
|
value: number;
|
||||||
@@ -44,15 +49,22 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
|
|||||||
Yup.object({
|
Yup.object({
|
||||||
id: Yup.number(),
|
id: Yup.number(),
|
||||||
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
|
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
|
||||||
kandang: Yup.object({
|
warehouse: Yup.object({
|
||||||
value: Yup.number()
|
value: Yup.number()
|
||||||
.min(1, 'Kandang wajib diisi!')
|
.min(1, 'Gudang fisik wajib diisi!')
|
||||||
.required('Kandang wajib diisi!'),
|
.required('Gudang fisik wajib diisi!'),
|
||||||
label: Yup.string().required('Kandang wajib diisi!'),
|
label: Yup.string().required('Gudang fisik wajib diisi!'),
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
kandang_id: Yup.number()
|
warehouse_id: Yup.number()
|
||||||
.min(1, 'Kandang wajib diisi!')
|
.min(1, 'Gudang fisik wajib diisi!')
|
||||||
.required('Kandang wajib diisi!'),
|
.required('Gudang fisik wajib diisi!'),
|
||||||
|
kandang: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
kandang_id: Yup.number().optional(),
|
||||||
product_warehouse: Yup.object({
|
product_warehouse: Yup.object({
|
||||||
value: Yup.number()
|
value: Yup.number()
|
||||||
.min(1, 'Produk wajib diisi!')
|
.min(1, 'Produk wajib diisi!')
|
||||||
|
|||||||
+146
-66
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||||
import { RefObject, useEffect, useMemo, useState } from 'react';
|
import { RefObject, useEffect, useMemo, useState } from 'react';
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
import { WarehouseApi } from '@/services/api/master-data';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
|
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
|
||||||
|
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
|
||||||
|
|
||||||
const SalesOrderProductForm = ({
|
const SalesOrderProductForm = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -67,7 +68,25 @@ const SalesOrderProductForm = ({
|
|||||||
? (Number(initialValues.total_price) -
|
? (Number(initialValues.total_price) -
|
||||||
initialSisaBerat * Number(initialValues.unit_price || 0)) /
|
initialSisaBerat * Number(initialValues.unit_price || 0)) /
|
||||||
Number(initialValues.total_peti)
|
Number(initialValues.total_peti)
|
||||||
: 0;
|
: Number(initialValues?.unit_price || 0);
|
||||||
|
|
||||||
|
const isInitialTelurQty =
|
||||||
|
initialValues?.marketing_type?.value?.toLowerCase() === 'telur' &&
|
||||||
|
initialValues?.convertion_unit?.value?.toLowerCase() === 'qty';
|
||||||
|
|
||||||
|
const initialUnitPrice =
|
||||||
|
isInitialTelurQty &&
|
||||||
|
Number(initialValues?.total_price || 0) > 0 &&
|
||||||
|
Number(initialValues?.qty || 0) > 0
|
||||||
|
? Number(initialValues?.total_price) / Number(initialValues?.qty)
|
||||||
|
: initialValues?.unit_price || '';
|
||||||
|
|
||||||
|
const initialPricePerQty =
|
||||||
|
isInitialTelurQty &&
|
||||||
|
Number(initialValues?.total_price || 0) > 0 &&
|
||||||
|
Number(initialValues?.total_weight || 0) > 0
|
||||||
|
? Number(initialValues?.total_price) / Number(initialValues?.total_weight)
|
||||||
|
: (initialValues?.price_per_qty ?? null);
|
||||||
|
|
||||||
const initialPriceSisaBerat =
|
const initialPriceSisaBerat =
|
||||||
initialValues?.total_price && initialValues?.total_peti
|
initialValues?.total_price && initialValues?.total_peti
|
||||||
@@ -84,11 +103,15 @@ const SalesOrderProductForm = ({
|
|||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
vehicle_number: initialValues?.vehicle_number || '',
|
vehicle_number: initialValues?.vehicle_number || '',
|
||||||
|
warehouse_id:
|
||||||
|
initialValues?.warehouse_id ?? initialValues?.kandang_id ?? undefined,
|
||||||
|
warehouse: initialValues?.warehouse ?? initialValues?.kandang ?? null,
|
||||||
kandang_id: initialValues?.kandang_id || undefined,
|
kandang_id: initialValues?.kandang_id || undefined,
|
||||||
kandang: initialValues?.kandang || null,
|
kandang: initialValues?.kandang || null,
|
||||||
product_warehouse: initialValues?.product_warehouse || null,
|
product_warehouse: initialValues?.product_warehouse || null,
|
||||||
|
product_warehouse_data: initialValues?.product_warehouse_data || null,
|
||||||
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
||||||
unit_price: initialValues?.unit_price || '',
|
unit_price: initialUnitPrice,
|
||||||
total_weight: initialValues?.total_weight || '',
|
total_weight: initialValues?.total_weight || '',
|
||||||
qty: initialValues?.qty || '',
|
qty: initialValues?.qty || '',
|
||||||
avg_weight: initialValues?.avg_weight || '',
|
avg_weight: initialValues?.avg_weight || '',
|
||||||
@@ -102,7 +125,7 @@ const SalesOrderProductForm = ({
|
|||||||
convertion_unit: initialValues?.convertion_unit || null,
|
convertion_unit: initialValues?.convertion_unit || null,
|
||||||
marketing_type: initialValues?.marketing_type || null,
|
marketing_type: initialValues?.marketing_type || null,
|
||||||
total_peti: initialValues?.total_peti ?? null,
|
total_peti: initialValues?.total_peti ?? null,
|
||||||
price_per_qty: initialValues?.price_per_qty ?? null,
|
price_per_qty: initialPricePerQty,
|
||||||
sisa_berat: initialSisaBerat,
|
sisa_berat: initialSisaBerat,
|
||||||
price_sisa_berat: initialPriceSisaBerat,
|
price_sisa_berat: initialPriceSisaBerat,
|
||||||
week: initialValues?.week ?? null,
|
week: initialValues?.week ?? null,
|
||||||
@@ -132,11 +155,11 @@ const SalesOrderProductForm = ({
|
|||||||
|
|
||||||
// ===== Options =====
|
// ===== Options =====
|
||||||
const {
|
const {
|
||||||
options: kandangSourceOptions,
|
options: warehouseOptions,
|
||||||
isLoadingOptions: isLoadingKandangSourceOptions,
|
isLoadingOptions: isLoadingWarehouseOptions,
|
||||||
setInputValue: setKandangInputValue,
|
setInputValue: setWarehouseSearchValue,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreWarehouses,
|
||||||
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
|
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// Options Week dari minggu 1 - 22
|
// Options Week dari minggu 1 - 22
|
||||||
// const optionsWeek = useMemo(() => {
|
// const optionsWeek = useMemo(() => {
|
||||||
@@ -147,7 +170,6 @@ const SalesOrderProductForm = ({
|
|||||||
// }, []);
|
// }, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: warehouseSourceOptions,
|
|
||||||
rawData: warehouseSourceRawData,
|
rawData: warehouseSourceRawData,
|
||||||
isLoadingOptions: isLoadingWarehouseSourceOptions,
|
isLoadingOptions: isLoadingWarehouseSourceOptions,
|
||||||
setInputValue: setWarehouseInputValue,
|
setInputValue: setWarehouseInputValue,
|
||||||
@@ -156,32 +178,69 @@ const SalesOrderProductForm = ({
|
|||||||
ProductWarehouseApi.basePath,
|
ProductWarehouseApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
'product.name',
|
'product.name',
|
||||||
'',
|
'search',
|
||||||
{
|
{
|
||||||
warehouse_id: formik.values.kandang_id?.toString() ?? '',
|
limit: '100',
|
||||||
|
available_only: 'true',
|
||||||
|
warehouse_id: formik.values.warehouse_id?.toString() ?? '',
|
||||||
type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '',
|
type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const productOptionsFiltered = useMemo(() => {
|
const productOptionsFiltered = useMemo(() => {
|
||||||
return warehouseSourceOptions.filter(
|
if (!isResponseSuccess(warehouseSourceRawData)) {
|
||||||
(product) =>
|
return initialValues?.product_warehouse
|
||||||
!exisitingValues
|
? [initialValues.product_warehouse]
|
||||||
?.map((item) => item.product_warehouse_id)
|
: [];
|
||||||
.includes(product.value)
|
}
|
||||||
|
|
||||||
|
const selectedProductIds = new Set(
|
||||||
|
exisitingValues
|
||||||
|
?.filter((item) => item.id !== initialValues?.id)
|
||||||
|
.map((item) => Number(item.product_warehouse_id))
|
||||||
|
.filter((item) => item > 0) ?? []
|
||||||
);
|
);
|
||||||
}, [warehouseSourceOptions, exisitingValues]);
|
|
||||||
|
const options = warehouseSourceRawData.data
|
||||||
|
.filter((item: ProductWarehouse) => !selectedProductIds.has(item.id))
|
||||||
|
.map((item: ProductWarehouse) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: getProductWarehouseOptionLabel(item),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (
|
||||||
|
initialValues?.product_warehouse &&
|
||||||
|
initialValues?.product_warehouse_id
|
||||||
|
) {
|
||||||
|
const exists = options.find(
|
||||||
|
(option) =>
|
||||||
|
Number(option.value) === Number(initialValues.product_warehouse_id)
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
options.push(initialValues.product_warehouse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [warehouseSourceRawData, exisitingValues, initialValues]);
|
||||||
|
|
||||||
// ===== Handler =====
|
// ===== Handler =====
|
||||||
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('kandang', val as OptionType);
|
const warehouse = (val as OptionType | null) ?? null;
|
||||||
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
|
|
||||||
|
formik.setFieldValue('warehouse', warehouse);
|
||||||
|
formik.setFieldValue('warehouse_id', warehouse?.value);
|
||||||
|
formik.setFieldValue('kandang', warehouse);
|
||||||
|
formik.setFieldValue('kandang_id', warehouse?.value);
|
||||||
formik.setFieldValue('product_warehouse_id', null);
|
formik.setFieldValue('product_warehouse_id', null);
|
||||||
formik.setFieldValue('product_warehouse', null);
|
formik.setFieldValue('product_warehouse', null);
|
||||||
|
formik.setFieldValue('product_warehouse_data', null);
|
||||||
formik.setFieldValue('qty', '');
|
formik.setFieldValue('qty', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const productWarehouseChangeHandler = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
formik.setFieldValue('product_warehouse', val as OptionType);
|
formik.setFieldValue('product_warehouse', val as OptionType);
|
||||||
const newId = (val as OptionType)?.value;
|
const newId = (val as OptionType)?.value;
|
||||||
formik.setFieldValue('product_warehouse_id', newId);
|
formik.setFieldValue('product_warehouse_id', newId);
|
||||||
@@ -191,7 +250,13 @@ const SalesOrderProductForm = ({
|
|||||||
(item: ProductWarehouse) => item.id === newId
|
(item: ProductWarehouse) => item.id === newId
|
||||||
);
|
);
|
||||||
setSelectedProductWarehouse(productWarehouse || null);
|
setSelectedProductWarehouse(productWarehouse || null);
|
||||||
|
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
|
||||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||||
|
|
||||||
|
if (productWarehouse?.quantity) {
|
||||||
|
handleFieldChange('qty', productWarehouse?.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
||||||
if (
|
if (
|
||||||
productWarehouse?.week !== undefined &&
|
productWarehouse?.week !== undefined &&
|
||||||
@@ -204,6 +269,8 @@ const SalesOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
handleBlurField('qty');
|
handleBlurField('qty');
|
||||||
} else {
|
} else {
|
||||||
|
setSelectedProductWarehouse(null);
|
||||||
|
formik.setFieldValue('product_warehouse_data', null);
|
||||||
formik.setFieldValue('qty', '');
|
formik.setFieldValue('qty', '');
|
||||||
formik.setFieldValue('uom', '');
|
formik.setFieldValue('uom', '');
|
||||||
formik.setFieldValue('week', null);
|
formik.setFieldValue('week', null);
|
||||||
@@ -217,9 +284,12 @@ const SalesOrderProductForm = ({
|
|||||||
formik.resetForm({
|
formik.resetForm({
|
||||||
values: {
|
values: {
|
||||||
vehicle_number: '',
|
vehicle_number: '',
|
||||||
|
warehouse_id: undefined,
|
||||||
|
warehouse: null,
|
||||||
kandang_id: undefined,
|
kandang_id: undefined,
|
||||||
kandang: null,
|
kandang: null,
|
||||||
product_warehouse: null,
|
product_warehouse: null,
|
||||||
|
product_warehouse_data: null,
|
||||||
product_warehouse_id: undefined,
|
product_warehouse_id: undefined,
|
||||||
unit_price: '',
|
unit_price: '',
|
||||||
total_weight: '',
|
total_weight: '',
|
||||||
@@ -310,6 +380,10 @@ const SalesOrderProductForm = ({
|
|||||||
handleBlurField('week');
|
handleBlurField('week');
|
||||||
}, [formik.values.week]);
|
}, [formik.values.week]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedProductWarehouse(initialValues?.product_warehouse_data || null);
|
||||||
|
}, [initialValues?.product_warehouse_data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form
|
<form
|
||||||
@@ -348,22 +422,22 @@ const SalesOrderProductForm = ({
|
|||||||
errorMessage={formik.errors.vehicle_number}
|
errorMessage={formik.errors.vehicle_number}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Gudang */}
|
{/* Gudang Fisik */}
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
required
|
required
|
||||||
label='Gudang'
|
label='Gudang Fisik'
|
||||||
options={kandangSourceOptions}
|
options={warehouseOptions}
|
||||||
isLoading={isLoadingKandangSourceOptions}
|
isLoading={isLoadingWarehouseOptions}
|
||||||
value={formik.values.kandang}
|
value={formik.values.warehouse}
|
||||||
onChange={kandangChangeHandler}
|
onChange={warehouseChangeHandler}
|
||||||
isClearable
|
isClearable
|
||||||
onInputChange={setKandangInputValue}
|
onInputChange={setWarehouseSearchValue}
|
||||||
onMenuScrollToBottom={loadMoreKandang}
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
formik.touched.warehouse_id && Boolean(formik.errors.warehouse_id)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.kandang_id}
|
errorMessage={formik.errors.warehouse_id}
|
||||||
placeholder='Pilih Gudang'
|
placeholder='Pilih Gudang Fisik'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Kategori */}
|
{/* Kategori */}
|
||||||
@@ -374,8 +448,9 @@ const SalesOrderProductForm = ({
|
|||||||
value={formik.values.marketing_type}
|
value={formik.values.marketing_type}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formik.setFieldValue('marketing_type', val);
|
formik.setFieldValue('marketing_type', val);
|
||||||
warehouseChangeHandler(null);
|
productWarehouseChangeHandler(null);
|
||||||
formik.setFieldValue('product_warehouse', null);
|
formik.setFieldValue('product_warehouse', null);
|
||||||
|
formik.setFieldValue('product_warehouse_data', null);
|
||||||
formik.setFieldValue('product_warehouse_id', null);
|
formik.setFieldValue('product_warehouse_id', null);
|
||||||
formik.setFieldValue('convertion_unit', null);
|
formik.setFieldValue('convertion_unit', null);
|
||||||
formik.setFieldValue('weight_per_convertion', null);
|
formik.setFieldValue('weight_per_convertion', null);
|
||||||
@@ -392,18 +467,18 @@ const SalesOrderProductForm = ({
|
|||||||
options={productOptionsFiltered}
|
options={productOptionsFiltered}
|
||||||
isLoading={isLoadingWarehouseSourceOptions}
|
isLoading={isLoadingWarehouseSourceOptions}
|
||||||
value={formik.values.product_warehouse}
|
value={formik.values.product_warehouse}
|
||||||
onChange={warehouseChangeHandler}
|
onChange={productWarehouseChangeHandler}
|
||||||
onInputChange={setWarehouseInputValue}
|
onInputChange={setWarehouseInputValue}
|
||||||
onMenuScrollToBottom={loadMoreWarehouse}
|
onMenuScrollToBottom={loadMoreWarehouse}
|
||||||
isClearable
|
isClearable
|
||||||
placeholder={
|
placeholder={
|
||||||
formik.values.kandang_id
|
formik.values.warehouse_id
|
||||||
? productOptionsFiltered.length == 0
|
? productOptionsFiltered.length == 0
|
||||||
? 'Tidak ada produk yang tersedia'
|
? 'Tidak ada produk yang tersedia'
|
||||||
: 'Pilih produk'
|
: 'Pilih produk'
|
||||||
: 'Pilih Kandang Terlebih Dahulu'
|
: 'Pilih Gudang Fisik Terlebih Dahulu'
|
||||||
}
|
}
|
||||||
isDisabled={!formik.values.kandang_id}
|
isDisabled={!formik.values.warehouse_id}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.product_warehouse_id &&
|
formik.touched.product_warehouse_id &&
|
||||||
Boolean(formik.errors.product_warehouse_id)
|
Boolean(formik.errors.product_warehouse_id)
|
||||||
@@ -471,7 +546,7 @@ const SalesOrderProductForm = ({
|
|||||||
<input
|
<input
|
||||||
type='radio'
|
type='radio'
|
||||||
checked={
|
checked={
|
||||||
formik.values.convertion_unit?.value ===
|
formik.values.convertion_unit?.value.toLowerCase() ===
|
||||||
option.value
|
option.value
|
||||||
}
|
}
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
@@ -494,7 +569,9 @@ const SalesOrderProductForm = ({
|
|||||||
} per ${formik.values.convertion_unit?.value}`}
|
} per ${formik.values.convertion_unit?.value}`}
|
||||||
value={formik.values.weight_per_convertion ?? ''}
|
value={formik.values.weight_per_convertion ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value)
|
||||||
|
? Number(e.target.value)
|
||||||
|
: '';
|
||||||
handleFieldChange('weight_per_convertion', value, () =>
|
handleFieldChange('weight_per_convertion', value, () =>
|
||||||
setCurrentInput(e.target.name)
|
setCurrentInput(e.target.name)
|
||||||
);
|
);
|
||||||
@@ -548,7 +625,7 @@ const SalesOrderProductForm = ({
|
|||||||
placeholder='Masukan Total Peti'
|
placeholder='Masukan Total Peti'
|
||||||
endAdornment={
|
endAdornment={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span className='text-sm text-base-content/50'>Kg</span>
|
<span className='text-sm text-base-content/50'>Peti</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
||||||
@@ -598,6 +675,9 @@ const SalesOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
errorMessage={formik.errors.total_weight}
|
errorMessage={formik.errors.total_weight}
|
||||||
placeholder='Masukan Total Bobot'
|
placeholder='Masukan Total Bobot'
|
||||||
|
disabled={
|
||||||
|
formik.values.convertion_unit?.value.toLowerCase() === 'peti'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -665,12 +745,34 @@ const SalesOrderProductForm = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Harga per butir untuk TELUR + QTY */}
|
{/* Harga Satuan per Uom Produk Warehouse */}
|
||||||
|
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
||||||
|
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
label={`Harga / ${formik.values.convertion_unit?.label.toLowerCase() !== 'qty' ? 'Kg' : 'Butir'} (Rp)`}
|
||||||
|
name='unit_price'
|
||||||
|
value={formik.values.unit_price}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
handleFieldChange('unit_price', value, () =>
|
||||||
|
setCurrentInput(e.target.name)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isError={
|
||||||
|
formik.touched.unit_price && Boolean(formik.errors.unit_price)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.unit_price}
|
||||||
|
placeholder='Masukan Harga Satuan...'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Harga per kg untuk TELUR + QTY */}
|
||||||
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
|
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
|
||||||
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
|
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga / Butir (Rp)'
|
label='Harga / Kg (Rp)'
|
||||||
name='price_per_qty'
|
name='price_per_qty'
|
||||||
value={formik.values.price_per_qty ?? undefined}
|
value={formik.values.price_per_qty ?? undefined}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -684,29 +786,7 @@ const SalesOrderProductForm = ({
|
|||||||
Boolean(formik.errors.price_per_qty)
|
Boolean(formik.errors.price_per_qty)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.price_per_qty}
|
errorMessage={formik.errors.price_per_qty}
|
||||||
placeholder='Masukan Harga per Butir'
|
placeholder='Masukan Harga per Kg'
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Harga Satuan per Uom Produk Warehouse */}
|
|
||||||
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
|
||||||
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
|
||||||
<NumberInput
|
|
||||||
required
|
|
||||||
label={`Harga / ${formik.values.convertion_unit?.label !== 'qty' ? 'Kg' : (selectedProductWarehouse?.product?.uom?.name ?? 'Produk')} (Rp)`}
|
|
||||||
name='unit_price'
|
|
||||||
value={formik.values.unit_price}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = Number(e.target.value);
|
|
||||||
handleFieldChange('unit_price', value, () =>
|
|
||||||
setCurrentInput(e.target.name)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
isError={
|
|
||||||
formik.touched.unit_price && Boolean(formik.errors.unit_price)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.unit_price}
|
|
||||||
placeholder='Masukan Harga Satuan'
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { Icon } from '@iconify/react';
|
|||||||
import { useRef, useMemo } from 'react';
|
import { useRef, useMemo } from 'react';
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
||||||
import { Marketing, BaseDelivery } from '@/types/api/marketing/marketing';
|
import { Marketing } from '@/types/api/marketing/marketing';
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
|
import { DeliveryProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm.schema';
|
||||||
|
|
||||||
type DeliveryOrderProductTableProps = {
|
type DeliveryOrderProductTableProps = {
|
||||||
data: DeliveryOrderProductFormValues[];
|
data: DeliveryOrderProductFormValues[];
|
||||||
@@ -55,14 +56,17 @@ const DeliveryOrderProductTable = ({
|
|||||||
|
|
||||||
const deliveryItems = useMemo(() => {
|
const deliveryItems = useMemo(() => {
|
||||||
if (!hasDeliveryOrder) return [];
|
if (!hasDeliveryOrder) return [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
marketing?.delivery_order?.flatMap((doItem) =>
|
marketing?.delivery_order?.flatMap((doItem) =>
|
||||||
doItem.deliveries.map((delivery) => ({
|
DeliveryProductToFieldValues(marketing?.sales_order, doItem).map(
|
||||||
...delivery,
|
(delivery) => ({
|
||||||
do_number: doItem.do_number,
|
...delivery,
|
||||||
delivery_date: doItem.delivery_date,
|
do_number: doItem.do_number,
|
||||||
warehouse: doItem.warehouse,
|
delivery_date: doItem.delivery_date,
|
||||||
}))
|
warehouse: doItem.warehouse,
|
||||||
|
})
|
||||||
|
)
|
||||||
) ?? []
|
) ?? []
|
||||||
);
|
);
|
||||||
}, [marketing?.delivery_order, hasDeliveryOrder]);
|
}, [marketing?.delivery_order, hasDeliveryOrder]);
|
||||||
@@ -81,7 +85,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||||
<div>Value</div>
|
<div>Value</div>
|
||||||
{formType !== 'success' &&
|
{/* {formType !== 'success' &&
|
||||||
(formType === 'add_delivery' ||
|
(formType === 'add_delivery' ||
|
||||||
formType === 'edit_delivery' ||
|
formType === 'edit_delivery' ||
|
||||||
formType === 'detail') && (
|
formType === 'detail') && (
|
||||||
@@ -98,15 +102,16 @@ const DeliveryOrderProductTable = ({
|
|||||||
<Icon icon='heroicons:pencil' width={20} height={20} />
|
<Icon icon='heroicons:pencil' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{doItem?.warehouse?.name ||
|
{doItem?.warehouse?.name ||
|
||||||
|
item.marketing_product?.warehouse?.label ||
|
||||||
item.marketing_product?.product_warehouse_data?.warehouse?.name}
|
item.marketing_product?.product_warehouse_data?.warehouse?.name}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -119,7 +124,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Qty</td>
|
<td className='text-sm px-4 py-3'>Qty</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{item.qty
|
{item.qty !== undefined && item.qty !== null && item.qty !== ''
|
||||||
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
@@ -136,12 +141,15 @@ const DeliveryOrderProductTable = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatNumber(Number(item.total_weight))}
|
{formatNumber(Number(item.total_weight))} Kg
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
<td className='text-sm px-4 py-3'>
|
||||||
|
Total Harga Satuan
|
||||||
|
{item.convertion_unit?.label.toLowerCase() === 'peti' && ' (Kg)'}
|
||||||
|
</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||||
</td>
|
</td>
|
||||||
@@ -211,7 +219,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderDeliveryOrderContent = (
|
const renderDeliveryOrderContent = (
|
||||||
item: BaseDelivery & {
|
item: DeliveryOrderProductFormValues & {
|
||||||
do_number: string;
|
do_number: string;
|
||||||
delivery_date: string;
|
delivery_date: string;
|
||||||
warehouse: Warehouse;
|
warehouse: Warehouse;
|
||||||
@@ -230,25 +238,43 @@ const DeliveryOrderProductTable = ({
|
|||||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||||
<div>Value</div>
|
<div>Value</div>
|
||||||
|
{formType !== 'success' &&
|
||||||
|
(formType === 'add_delivery' ||
|
||||||
|
formType === 'edit_delivery' ||
|
||||||
|
formType === 'detail') && (
|
||||||
|
<div className='flex flex-row gap-1.5 items-center'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
onEditRef.current(item.id as number, item);
|
||||||
|
}}
|
||||||
|
className='p-0 hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:pencil' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
|
||||||
<td className='text-sm px-4 py-3'>{item.warehouse?.name}</td>
|
<td className='text-sm px-4 py-3'>{item.warehouse?.name}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Produk</td>
|
<td className='text-sm px-4 py-3'>Produk</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{item.product_warehouse?.product?.name}
|
{item.marketing_product?.product_warehouse_data?.product.name}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Qty</td>
|
<td className='text-sm px-4 py-3'>Qty</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{item.qty
|
{item.qty !== undefined && item.qty !== null && item.qty !== ''
|
||||||
? `${formatNumber(item.qty)} ${item.product_warehouse?.product?.uom?.name ?? ''}`
|
? `${formatNumber(Number(item.qty))} ${item.marketing_product?.product_warehouse_data?.product.uom.name ?? ''}`
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -271,13 +297,13 @@ const DeliveryOrderProductTable = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatCurrency(item.unit_price)}
|
{formatCurrency(Number(item.unit_price))}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatCurrency(item.total_price)}
|
{formatCurrency(Number(item.total_price))}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
@@ -333,7 +359,9 @@ const DeliveryOrderProductTable = ({
|
|||||||
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
||||||
{hasDeliveryOrder
|
{hasDeliveryOrder
|
||||||
? deliveryItems.map((item, index) => (
|
? deliveryItems.map((item, index) => (
|
||||||
<div key={`do-table-${item.product_warehouse?.id}-${index}`}>
|
<div
|
||||||
|
key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
|
||||||
|
>
|
||||||
{formType === 'success' ? (
|
{formType === 'success' ? (
|
||||||
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
|
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
|
||||||
<table
|
<table
|
||||||
@@ -349,8 +377,11 @@ const DeliveryOrderProductTable = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Card
|
<Card
|
||||||
key={`do-table-${item.product_warehouse?.id}-${index}`}
|
key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
|
||||||
title={item.product_warehouse?.product?.name || 'Produk'}
|
title={
|
||||||
|
item.marketing_product?.product_warehouse_data?.product
|
||||||
|
.name || 'Produk'
|
||||||
|
}
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
defaultCollapsed={false}
|
defaultCollapsed={false}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ const SalesOrderProductTable = ({
|
|||||||
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
|
||||||
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{item.warehouse?.label ?? item.kandang?.label}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Kategori</td>
|
<td className='text-sm px-4 py-3'>Kategori</td>
|
||||||
@@ -135,8 +137,22 @@ const SalesOrderProductTable = ({
|
|||||||
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Harga Satuan Per Peti</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{formatCurrency(
|
||||||
|
parseFloat(item.unit_price as string) *
|
||||||
|
parseFloat(String(item.weight_per_convertion))
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Harga Satuan</td>
|
<td className='text-sm px-4 py-3'>
|
||||||
|
Harga Satuan
|
||||||
|
{item.convertion_unit?.value.toLowerCase() === 'peti' && ' (Kg)'}
|
||||||
|
</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ const PDFDocument = ({
|
|||||||
<View style={pdfStyles.header}>
|
<View style={pdfStyles.header}>
|
||||||
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||||
<Text style={pdfStyles.address}>
|
<Text style={pdfStyles.address}>
|
||||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
|
||||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
Bandung Barat, Jawa Barat 40514
|
||||||
</Text>
|
</Text>
|
||||||
<View style={pdfStyles.divider} />
|
<View style={pdfStyles.divider} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ const PDFDocument = ({ data }: { data: Marketing }) => {
|
|||||||
<View style={pdfStyles.header}>
|
<View style={pdfStyles.header}>
|
||||||
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||||
<Text style={pdfStyles.address}>
|
<Text style={pdfStyles.address}>
|
||||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
|
||||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
Bandung Barat, Jawa Barat 40514
|
||||||
</Text>
|
</Text>
|
||||||
<View style={pdfStyles.divider} />
|
<View style={pdfStyles.divider} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Area } from '@/types/api/master-data/area';
|
|||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AreasTable = () => {
|
const AreasTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,12 +109,14 @@ const AreasTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'areas-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -137,17 +134,8 @@ const AreasTable = () => {
|
|||||||
const [selectedArea, setSelectedArea] = useState<Area | undefined>(undefined);
|
const [selectedArea, setSelectedArea] = useState<Area | undefined>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('areas-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Bank } from '@/types/api/master-data/bank';
|
|||||||
import { BankApi } from '@/services/api/master-data';
|
import { BankApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BanksTable = () => {
|
const BanksTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,12 +109,14 @@ const BanksTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'banks-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -137,17 +134,8 @@ const BanksTable = () => {
|
|||||||
const [selectedBank, setSelectedBank] = useState<Bank | undefined>(undefined);
|
const [selectedBank, setSelectedBank] = useState<Bank | undefined>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('banks-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Customer } from '@/types/api/master-data/customer';
|
|||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CustomersTable = () => {
|
const CustomersTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,12 +109,14 @@ const CustomersTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'customers-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -139,17 +136,8 @@ const CustomersTable = () => {
|
|||||||
>(undefined);
|
>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('customers-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -201,6 +189,11 @@ const CustomersTable = () => {
|
|||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'bank_name',
|
||||||
|
header: 'Nama Bank',
|
||||||
|
cell: (props) => props.row.original.bank_name || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props: CellContext<Customer, unknown>) => {
|
cell: (props: CellContext<Customer, unknown>) => {
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export const CustomerFormSchema = Yup.object({
|
|||||||
.email('Format email tidak valid!')
|
.email('Format email tidak valid!')
|
||||||
.required('Email wajib diisi!'),
|
.required('Email wajib diisi!'),
|
||||||
|
|
||||||
|
bank_name: Yup.string()
|
||||||
|
.min(3, 'Nama bank minimal 3 karakter!')
|
||||||
|
.required('Nama bank wajib diisi!'),
|
||||||
account_number: Yup.string()
|
account_number: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
||||||
.required('Nomor rekening wajib diisi!'),
|
.required('Nomor rekening wajib diisi!'),
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ const CustomerForm = ({
|
|||||||
},
|
},
|
||||||
type: normalizeType(initialValues?.type),
|
type: normalizeType(initialValues?.type),
|
||||||
address: initialValues?.address ?? '',
|
address: initialValues?.address ?? '',
|
||||||
|
bank_name: initialValues?.bank_name ?? '',
|
||||||
account_number: initialValues?.account_number ?? '',
|
account_number: initialValues?.account_number ?? '',
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
@@ -164,6 +165,7 @@ const CustomerForm = ({
|
|||||||
pic_id: values.picId,
|
pic_id: values.picId,
|
||||||
type: (values.type as OptionType).value as string,
|
type: (values.type as OptionType).value as string,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
|
bank_name: values.bank_name,
|
||||||
account_number: values.account_number,
|
account_number: values.account_number,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,6 +288,22 @@ const CustomerForm = ({
|
|||||||
errorMessage={formik.errors.phone}
|
errorMessage={formik.errors.phone}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Nama Bank'
|
||||||
|
name='bank_name'
|
||||||
|
placeholder='Masukkan nama bank customer'
|
||||||
|
value={formik.values.bank_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
formik.setFieldValue('bank_name', e.target.value.toUpperCase())
|
||||||
|
}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.bank_name && Boolean(formik.errors.bank_name)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.bank_name}
|
||||||
|
readOnly={formType === 'detail'}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Rekening'
|
label='Nomor Rekening'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Flock } from '@/types/api/master-data/flock';
|
|||||||
import { FlockApi } from '@/services/api/master-data';
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FlockTable = () => {
|
const FlockTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,12 +109,14 @@ const FlockTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'flock-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -139,17 +136,8 @@ const FlockTable = () => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('flocks-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -35,7 +28,6 @@ import { User } from '@/types/api/api-general';
|
|||||||
import { formatNumber } from '@/lib/helper';
|
import { formatNumber } from '@/lib/helper';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
KandangFilterSchema,
|
KandangFilterSchema,
|
||||||
KandangFilterType,
|
KandangFilterType,
|
||||||
@@ -122,20 +114,21 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const KandangsTable = () => {
|
const KandangsTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
locationFilter?: OptionType<string>;
|
||||||
|
picFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
locationFilter: '',
|
locationFilter: undefined,
|
||||||
picFilter: '',
|
picFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -143,6 +136,8 @@ const KandangsTable = () => {
|
|||||||
locationFilter: 'location_id',
|
locationFilter: 'location_id',
|
||||||
picFilter: 'pic_id',
|
picFilter: 'pic_id',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'kandangs-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -151,22 +146,34 @@ const KandangsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<KandangFilterType>({
|
const formik = useFormik<KandangFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
location_id: null,
|
location: tableFilterState.locationFilter,
|
||||||
pic_id: null,
|
pic: tableFilterState.picFilter,
|
||||||
},
|
},
|
||||||
validationSchema: KandangFilterSchema,
|
validationSchema: KandangFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('locationFilter', values.location_id || '');
|
updateFilter('locationFilter', values.location || undefined, true);
|
||||||
updateFilter('picFilter', values.pic_id || '');
|
updateFilter('picFilter', values.pic || undefined, true);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('locationFilter', '');
|
|
||||||
updateFilter('picFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('locationFilter', undefined, true);
|
||||||
|
updateFilter('picFilter', undefined, true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
location: undefined,
|
||||||
|
pic: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { setFieldValue } = formik;
|
||||||
|
|
||||||
// ===== LOCATION OPTIONS =====
|
// ===== LOCATION OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
@@ -194,43 +201,15 @@ const KandangsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterLocationChange = useCallback(
|
const handleFilterLocationChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const location = val as OptionType | null;
|
) => {
|
||||||
const locationId = location?.value ? String(location.value) : null;
|
setFieldValue('location', val);
|
||||||
|
};
|
||||||
|
|
||||||
formik.setFieldValue('location_id', locationId);
|
const handleFilterPicChange = (val: OptionType | OptionType[] | null) => {
|
||||||
},
|
setFieldValue('pic', val);
|
||||||
[formik]
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterPicChange = useCallback(
|
|
||||||
(val: OptionType | OptionType[] | null) => {
|
|
||||||
const pic = val as OptionType | null;
|
|
||||||
const picId = pic?.value ? String(pic.value) : null;
|
|
||||||
|
|
||||||
formik.setFieldValue('pic_id', picId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const locationIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.location_id) return null;
|
|
||||||
return (
|
|
||||||
locationOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.location_id, locationOptions]);
|
|
||||||
|
|
||||||
const picIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.pic_id) return null;
|
|
||||||
return (
|
|
||||||
picOptions.find((opt) => String(opt.value) === formik.values.pic_id) ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}, [formik.values.pic_id, picOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -255,17 +234,8 @@ const KandangsTable = () => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('kandangs-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -314,6 +284,10 @@ const KandangsTable = () => {
|
|||||||
accessorFn: (row) => row.pic?.name ?? '-',
|
accessorFn: (row) => row.pic?.name ?? '-',
|
||||||
header: 'PIC',
|
header: 'PIC',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => row.kandang_group?.name ?? '-',
|
||||||
|
header: 'Kandang Group',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props: CellContext<Kandang, unknown>) => {
|
cell: (props: CellContext<Kandang, unknown>) => {
|
||||||
@@ -471,13 +445,13 @@ const KandangsTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={locationIdValue}
|
value={formik.values.location}
|
||||||
onChange={handleFilterLocationChange}
|
onChange={handleFilterLocationChange}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
@@ -490,7 +464,7 @@ const KandangsTable = () => {
|
|||||||
label='PIC'
|
label='PIC'
|
||||||
placeholder='Pilih PIC'
|
placeholder='Pilih PIC'
|
||||||
options={picOptions}
|
options={picOptions}
|
||||||
value={picIdValue}
|
value={formik.values.pic}
|
||||||
onChange={handleFilterPicChange}
|
onChange={handleFilterPicChange}
|
||||||
onInputChange={setPicInputValue}
|
onInputChange={setPicInputValue}
|
||||||
isLoading={isLoadingPicOptions}
|
isLoading={isLoadingPicOptions}
|
||||||
@@ -506,17 +480,14 @@ const KandangsTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
onClick={formikResetHandler}
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={!formik.isValid || formik.isSubmitting}
|
disabled={!formik.isValid}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const KandangFilterSchema = object().shape({
|
export const KandangFilterSchema = Yup.object().shape({
|
||||||
location_id: string().nullable(),
|
location: Yup.object({
|
||||||
pic_id: string().nullable(),
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
|
||||||
|
pic: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type KandangFilterType = {
|
export type KandangFilterType = {
|
||||||
location_id: string | null;
|
location?: OptionType<string>;
|
||||||
pic_id: string | null;
|
pic?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
type KandangFormSchemaType = {
|
type KandangFormSchemaType = {
|
||||||
@@ -19,6 +20,7 @@ type KandangFormSchemaType = {
|
|||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
| null;
|
| null;
|
||||||
|
group?: OptionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
||||||
@@ -42,6 +44,11 @@ export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
|||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
|
|
||||||
|
group: Yup.object({
|
||||||
|
value: Yup.number().min(1).required('Kandang Grup wajib diisi!'),
|
||||||
|
label: Yup.string().required('Kandang Grup wajib diisi!'),
|
||||||
|
}).required('Kandang Grup wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateKandangFormSchema = KandangFormSchema;
|
export const UpdateKandangFormSchema = KandangFormSchema;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useFormik } from 'formik';
|
import { getIn, useFormik } from 'formik';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -34,6 +34,8 @@ import NumberInput from '@/components/input/NumberInput';
|
|||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { User } from '@/types/api/api-general';
|
import { User } from '@/types/api/api-general';
|
||||||
|
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
|
||||||
interface KandangFormProps {
|
interface KandangFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -96,6 +98,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
label: initialValues.pic.name,
|
label: initialValues.pic.name,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
group: initialValues?.kandang_group
|
||||||
|
? {
|
||||||
|
value: initialValues.kandang_group.id,
|
||||||
|
label: initialValues.kandang_group.name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
|
|
||||||
@@ -111,6 +119,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
location_id: values.locationId!,
|
location_id: values.locationId!,
|
||||||
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
||||||
pic_id: values.picId!,
|
pic_id: values.picId!,
|
||||||
|
group_id: values.group?.value as number,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -162,6 +171,23 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
formik.setFieldValue('picId', (val as OptionType)?.value);
|
formik.setFieldValue('picId', (val as OptionType)?.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kandang Group
|
||||||
|
const {
|
||||||
|
setInputValue: setKandangGroupSelectInputValue,
|
||||||
|
options: kandangGroupOptions,
|
||||||
|
isLoadingOptions: isLoadingKandangGroupOptions,
|
||||||
|
loadMore: loadMoreKandangGroups,
|
||||||
|
} = useSelect<DailyChecklistKandang>(
|
||||||
|
DailyChecklistKandangApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name'
|
||||||
|
);
|
||||||
|
|
||||||
|
const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched('group', true);
|
||||||
|
formik.setFieldValue('group', val);
|
||||||
|
};
|
||||||
|
|
||||||
const deleteKandangClickHandler = () => {
|
const deleteKandangClickHandler = () => {
|
||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
};
|
};
|
||||||
@@ -269,6 +295,24 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
required
|
||||||
|
label='Kandang Group'
|
||||||
|
value={formik.values.group ?? undefined}
|
||||||
|
onChange={kandangGroupChangeHandler}
|
||||||
|
options={kandangGroupOptions}
|
||||||
|
onInputChange={setKandangGroupSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandangGroups}
|
||||||
|
isLoading={isLoadingKandangGroupOptions}
|
||||||
|
isError={formik.touched.group && Boolean(formik.errors.group)}
|
||||||
|
errorMessage={
|
||||||
|
getIn(formik.errors.group, 'value') ??
|
||||||
|
(formik.errors.group as string)
|
||||||
|
}
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -32,7 +25,6 @@ import { Area } from '@/types/api/master-data/area';
|
|||||||
import { LocationApi, AreaApi } from '@/services/api/master-data';
|
import { LocationApi, AreaApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
LocationFilterSchema,
|
LocationFilterSchema,
|
||||||
LocationFilterType,
|
LocationFilterType,
|
||||||
@@ -118,25 +110,27 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LocationsTable = () => {
|
const LocationsTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
areaFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
areaFilter: '',
|
areaFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
areaFilter: 'area_id',
|
areaFilter: 'area_id',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'locations-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -145,19 +139,28 @@ const LocationsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<LocationFilterType>({
|
const formik = useFormik<LocationFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: null,
|
area: tableFilterState.areaFilter,
|
||||||
},
|
},
|
||||||
validationSchema: LocationFilterSchema,
|
validationSchema: LocationFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id || '');
|
updateFilter('areaFilter', values.area || undefined, true);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('areaFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('areaFilter', undefined, true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
area: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
// ===== AREA OPTIONS =====
|
// ===== AREA OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setAreaInputValue,
|
setInputValue: setAreaInputValue,
|
||||||
@@ -172,24 +175,9 @@ const LocationsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterAreaChange = useCallback(
|
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('area', val);
|
||||||
const area = val as OptionType | null;
|
};
|
||||||
const areaId = area?.value ? String(area.value) : null;
|
|
||||||
|
|
||||||
formik.setFieldValue('area_id', areaId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const areaIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.area_id) return null;
|
|
||||||
return (
|
|
||||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}, [formik.values.area_id, areaOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -212,19 +200,10 @@ const LocationsTable = () => {
|
|||||||
>(undefined);
|
>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('locations-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -425,13 +404,13 @@ const LocationsTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={areaIdValue}
|
value={formik.values.area}
|
||||||
onChange={handleFilterAreaChange}
|
onChange={handleFilterAreaChange}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
isLoading={isLoadingAreaOptions}
|
isLoading={isLoadingAreaOptions}
|
||||||
@@ -447,10 +426,7 @@ const LocationsTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
onClick={formikResetHandler}
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const LocationFilterSchema = object().shape({
|
export const LocationFilterSchema = Yup.object().shape({
|
||||||
area_id: string().nullable(),
|
area: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LocationFilterType = {
|
export type LocationFilterType = {
|
||||||
area_id: string | null;
|
area?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import { Nonstock } from '@/types/api/master-data/nonstock';
|
|||||||
import { NonstockApi } from '@/services/api/master-data';
|
import { NonstockApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NonstocksTable = () => {
|
const NonstocksTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,22 +109,16 @@ const NonstocksTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'nonstock-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('nonstocks-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -148,8 +137,7 @@ const NonstocksTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -21,7 +20,6 @@ import { ProductCategory } from '@/types/api/master-data/product-category';
|
|||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProductCategoryTable = () => {
|
const ProductCategoryTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -120,12 +115,10 @@ const ProductCategoryTable = () => {
|
|||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'product-category-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -144,8 +137,7 @@ const ProductCategoryTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -214,10 +206,6 @@ const ProductCategoryTable = () => {
|
|||||||
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
|
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('product-category-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -33,7 +26,6 @@ import { ProductApi, ProductCategoryApi } from '@/services/api/master-data';
|
|||||||
import { formatCurrency } from '@/lib/helper';
|
import { formatCurrency } from '@/lib/helper';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
ProductFilterSchema,
|
ProductFilterSchema,
|
||||||
ProductFilterType,
|
ProductFilterType,
|
||||||
@@ -119,25 +111,27 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProductsTable = () => {
|
const ProductsTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
productCategoryFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
productCategoryFilter: '',
|
productCategoryFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
productCategoryFilter: 'product_category_id',
|
productCategoryFilter: 'product_category_id',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'product-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -146,19 +140,32 @@ const ProductsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<ProductFilterType>({
|
const formik = useFormik<ProductFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product_category_id: null,
|
product_category: tableFilterState.productCategoryFilter,
|
||||||
},
|
},
|
||||||
validationSchema: ProductFilterSchema,
|
validationSchema: ProductFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productCategoryFilter', values.product_category_id || '');
|
updateFilter(
|
||||||
|
'productCategoryFilter',
|
||||||
|
values.product_category || undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('productCategoryFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('productCategoryFilter', undefined, true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
product_category: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
// ===== PRODUCT CATEGORY OPTIONS =====
|
// ===== PRODUCT CATEGORY OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setProductCategoryInputValue,
|
setInputValue: setProductCategoryInputValue,
|
||||||
@@ -173,25 +180,11 @@ const ProductsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductCategoryChange = useCallback(
|
const handleFilterProductCategoryChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const category = val as OptionType | null;
|
) => {
|
||||||
const categoryId = category?.value ? String(category.value) : null;
|
formik.setFieldValue('product_category', val);
|
||||||
|
};
|
||||||
formik.setFieldValue('product_category_id', categoryId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const productCategoryIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.product_category_id) return null;
|
|
||||||
return (
|
|
||||||
productCategoryOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.product_category_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.product_category_id, productCategoryOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -199,10 +192,6 @@ const ProductsTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -220,13 +209,8 @@ const ProductsTable = () => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('product-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -477,13 +461,13 @@ const ProductsTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Kategori Produk'
|
label='Kategori Produk'
|
||||||
placeholder='Pilih Kategori Produk'
|
placeholder='Pilih Kategori Produk'
|
||||||
options={productCategoryOptions}
|
options={productCategoryOptions}
|
||||||
value={productCategoryIdValue}
|
value={formik.values.product_category}
|
||||||
onChange={handleFilterProductCategoryChange}
|
onChange={handleFilterProductCategoryChange}
|
||||||
onInputChange={setProductCategoryInputValue}
|
onInputChange={setProductCategoryInputValue}
|
||||||
isLoading={isLoadingProductCategoryOptions}
|
isLoading={isLoadingProductCategoryOptions}
|
||||||
@@ -499,10 +483,7 @@ const ProductsTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
onClick={formikResetHandler}
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const ProductFilterSchema = object().shape({
|
export const ProductFilterSchema = Yup.object().shape({
|
||||||
product_category_id: string().nullable(),
|
product_category: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProductFilterType = {
|
export type ProductFilterType = {
|
||||||
product_category_id: string | null;
|
product_category?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -154,17 +154,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
sku: values.sku,
|
sku: values.sku,
|
||||||
uom_id: values.uom_id,
|
uom_id: values.uom_id,
|
||||||
product_category_id: values.product_category_id,
|
product_category_id: values.product_category_id,
|
||||||
product_price: parseInt(values.product_price.toString()) || 0,
|
product_price: parseFloat(values.product_price.toString()) || 0,
|
||||||
selling_price: values.selling_price
|
selling_price: values.selling_price
|
||||||
? parseInt(values.selling_price.toString()) || 0
|
? parseFloat(values.selling_price.toString()) || 0
|
||||||
: undefined,
|
: undefined,
|
||||||
tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
|
tax: values.tax ? parseFloat(values.tax.toString()) || 0 : undefined,
|
||||||
expiry_period: values.expiry_period
|
expiry_period: values.expiry_period
|
||||||
? parseInt(values.expiry_period.toString()) || 0
|
? parseFloat(values.expiry_period.toString()) || 0
|
||||||
: undefined,
|
: undefined,
|
||||||
suppliers: values.suppliers.map((s) => ({
|
suppliers: values.suppliers.map((s) => ({
|
||||||
supplier_id: s.supplier?.value as number,
|
supplier_id: s.supplier?.value as number,
|
||||||
price: parseInt(s.price.toString()) || 0,
|
price: parseFloat(s.price.toString()) || 0,
|
||||||
})),
|
})),
|
||||||
flag: values.flag,
|
flag: values.flag,
|
||||||
sub_flags: values.sub_flags,
|
sub_flags: values.sub_flags,
|
||||||
|
|||||||
@@ -128,27 +128,44 @@ const ProductionStandardTable = () => {
|
|||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
projectCategoryFilter: 'project_category',
|
projectCategoryFilter: 'project_category',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'production-standard-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FILTER INITIAL VALUES (derived from persisted state) =====
|
||||||
|
const filterInitialValues = useMemo<ProductionStandardFilterType>(
|
||||||
|
() => ({
|
||||||
|
project_category: tableFilterState.projectCategoryFilter || null,
|
||||||
|
}),
|
||||||
|
[tableFilterState.projectCategoryFilter]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<ProductionStandardFilterType>({
|
const formik = useFormik<ProductionStandardFilterType>({
|
||||||
initialValues: {
|
initialValues: filterInitialValues,
|
||||||
project_category: null,
|
|
||||||
},
|
|
||||||
validationSchema: ProductionStandardFilterSchema,
|
validationSchema: ProductionStandardFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('projectCategoryFilter', values.project_category || '');
|
updateFilter('projectCategoryFilter', values.project_category || '');
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('projectCategoryFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('projectCategoryFilter', '', true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
project_category: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
// ===== PROJECT CATEGORY OPTIONS (GROWING or LAYING) =====
|
// ===== PROJECT CATEGORY OPTIONS (GROWING or LAYING) =====
|
||||||
const projectCategoryOptions = useMemo(
|
const projectCategoryOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -381,7 +398,7 @@ const ProductionStandardTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
label='Kategori'
|
label='Kategori'
|
||||||
@@ -397,13 +414,9 @@ const ProductionStandardTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -30,7 +29,7 @@ import { Supplier } from '@/types/api/master-data/supplier';
|
|||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
SupplierFilterSchema,
|
SupplierFilterSchema,
|
||||||
SupplierFilterType,
|
SupplierFilterType,
|
||||||
@@ -117,20 +116,21 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SuppliersTable = () => {
|
const SuppliersTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
categoryFilter?: OptionType<string>;
|
||||||
|
flagFilter?: string;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
categoryFilter: '',
|
categoryFilter: undefined,
|
||||||
flagFilter: '',
|
flagFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -138,6 +138,8 @@ const SuppliersTable = () => {
|
|||||||
categoryFilter: 'category_id',
|
categoryFilter: 'category_id',
|
||||||
flagFilter: 'flag',
|
flagFilter: 'flag',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'supplier-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -146,26 +148,33 @@ const SuppliersTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<SupplierFilterType>({
|
const formik = useFormik<SupplierFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
category_id: null,
|
category: tableFilterState.categoryFilter,
|
||||||
flag: false,
|
flag: tableFilterState.flagFilter === 'EKSPEDISI',
|
||||||
},
|
},
|
||||||
validationSchema: SupplierFilterSchema,
|
validationSchema: SupplierFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('categoryFilter', values.category_id || '');
|
updateFilter('categoryFilter', values.category || undefined, true);
|
||||||
updateFilter(
|
updateFilter('flagFilter', values.flag === true ? 'EKSPEDISI' : '', true);
|
||||||
'flagFilter',
|
|
||||||
values.flag === true ? 'EKSPEDISI' : values.flag === false ? '' : ''
|
|
||||||
);
|
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('categoryFilter', '');
|
|
||||||
updateFilter('flagFilter', '');
|
|
||||||
formik.setFieldValue('flag', false);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('categoryFilter', undefined, true);
|
||||||
|
updateFilter('flagFilter', '', true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
category: undefined,
|
||||||
|
flag: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
const { setFieldValue } = formik;
|
const { setFieldValue } = formik;
|
||||||
|
|
||||||
// ===== CATEGORY OPTIONS (SAPRONAK or BOP) =====
|
// ===== CATEGORY OPTIONS (SAPRONAK or BOP) =====
|
||||||
@@ -187,15 +196,11 @@ const SuppliersTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterCategoryChange = useCallback(
|
const handleFilterCategoryChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const option = val as OptionType | null;
|
) => {
|
||||||
const categoryId = option?.value ? String(option.value) : null;
|
setFieldValue('category', val);
|
||||||
|
};
|
||||||
setFieldValue('category_id', categoryId);
|
|
||||||
},
|
|
||||||
[setFieldValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterFlagChange = useCallback(
|
const handleFilterFlagChange = useCallback(
|
||||||
(val: OptionType | OptionType[] | null) => {
|
(val: OptionType | OptionType[] | null) => {
|
||||||
@@ -213,13 +218,13 @@ const SuppliersTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
// ===== FILTER HELPERS =====
|
||||||
const categoryIdValue = useMemo(() => {
|
// const categoryIdValue = useMemo(() => {
|
||||||
if (!formik.values.category_id) return null;
|
// if (!formik.values.category_id) return null;
|
||||||
return (
|
// return (
|
||||||
categoryOptions.find((opt) => opt.value === formik.values.category_id) ||
|
// categoryOptions.find((opt) => opt.value === formik.values.category_id) ||
|
||||||
null
|
// null
|
||||||
);
|
// );
|
||||||
}, [formik.values.category_id, categoryOptions]);
|
// }, [formik.values.category_id, categoryOptions]);
|
||||||
|
|
||||||
const flagValue = useMemo(() => {
|
const flagValue = useMemo(() => {
|
||||||
if (formik.values.flag === null) return null;
|
if (formik.values.flag === null) return null;
|
||||||
@@ -243,14 +248,6 @@ const SuppliersTable = () => {
|
|||||||
}
|
}
|
||||||
}, [filterModal.open, tableFilterState.flagFilter, setFieldValue]);
|
}, [filterModal.open, tableFilterState.flagFilter, setFieldValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('suppliers-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -269,8 +266,7 @@ const SuppliersTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -330,6 +326,11 @@ const SuppliersTable = () => {
|
|||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'bank_name',
|
||||||
|
header: 'Nama Bank',
|
||||||
|
cell: (props) => props.row.original.bank_name || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'address',
|
accessorKey: 'address',
|
||||||
header: 'Alamat',
|
header: 'Alamat',
|
||||||
@@ -491,13 +492,13 @@ const SuppliersTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
label='Kategori'
|
label='Kategori'
|
||||||
placeholder='Pilih Kategori'
|
placeholder='Pilih Kategori'
|
||||||
options={categoryOptions}
|
options={categoryOptions}
|
||||||
value={categoryIdValue}
|
value={formik.values.category}
|
||||||
onChange={handleFilterCategoryChange}
|
onChange={handleFilterCategoryChange}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -517,13 +518,9 @@ const SuppliersTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { string, boolean, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const SupplierFilterSchema = object().shape({
|
export const SupplierFilterSchema = Yup.object().shape({
|
||||||
category_id: string().nullable(),
|
category: Yup.object({
|
||||||
flag: boolean().nullable(),
|
value: Yup.string().required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
|
||||||
|
flag: Yup.boolean().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SupplierFilterType = {
|
export type SupplierFilterType = {
|
||||||
category_id: string | null;
|
category?: OptionType<string>;
|
||||||
flag: boolean | null;
|
flag: boolean | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export const SupplierFormSchema = Yup.object({
|
|||||||
npwp: Yup.string()
|
npwp: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
|
||||||
.required('Nomor NPWP wajib diisi!'),
|
.required('Nomor NPWP wajib diisi!'),
|
||||||
|
bank_name: Yup.string()
|
||||||
|
.min(3, 'Nama bank minimal 3 karakter!')
|
||||||
|
.required('Nama bank wajib diisi!'),
|
||||||
account_number: Yup.string()
|
account_number: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
||||||
.required('Nomor rekening wajib diisi!'),
|
.required('Nomor rekening wajib diisi!'),
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const SupplierForm = ({
|
|||||||
email: initialValues?.email ?? '',
|
email: initialValues?.email ?? '',
|
||||||
address: initialValues?.address ?? '',
|
address: initialValues?.address ?? '',
|
||||||
npwp: initialValues?.npwp ?? '',
|
npwp: initialValues?.npwp ?? '',
|
||||||
|
bank_name: initialValues?.bank_name ?? '',
|
||||||
account_number: initialValues?.account_number ?? '',
|
account_number: initialValues?.account_number ?? '',
|
||||||
due_date: initialValues?.due_date ?? 1,
|
due_date: initialValues?.due_date ?? 1,
|
||||||
};
|
};
|
||||||
@@ -149,6 +150,7 @@ const SupplierForm = ({
|
|||||||
email: values.email,
|
email: values.email,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
npwp: values.npwp,
|
npwp: values.npwp,
|
||||||
|
bank_name: values.bank_name,
|
||||||
account_number: values.account_number,
|
account_number: values.account_number,
|
||||||
due_date: parseInt(values.due_date.toString()),
|
due_date: parseInt(values.due_date.toString()),
|
||||||
};
|
};
|
||||||
@@ -368,6 +370,22 @@ const SupplierForm = ({
|
|||||||
errorMessage={formik.errors.npwp}
|
errorMessage={formik.errors.npwp}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Nama Bank'
|
||||||
|
name='bank_name'
|
||||||
|
placeholder='Masukkan nama bank supplier'
|
||||||
|
value={formik.values.bank_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
formik.setFieldValue('bank_name', e.target.value.toUpperCase())
|
||||||
|
}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.bank_name && Boolean(formik.errors.bank_name)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.bank_name}
|
||||||
|
readOnly={formType === 'detail'}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Rekening'
|
label='Nomor Rekening'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Uom } from '@/types/api/master-data/uom';
|
|||||||
import { UomApi } from '@/services/api/master-data';
|
import { UomApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UomsTable = () => {
|
const UomsTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,22 +109,16 @@ const UomsTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'uom-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('uoms-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -146,8 +135,7 @@ const UomsTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -31,7 +24,6 @@ import { Warehouse } from '@/types/api/master-data/warehouse';
|
|||||||
import { WarehouseApi, AreaApi } from '@/services/api/master-data';
|
import { WarehouseApi, AreaApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
WarehouseFilterSchema,
|
WarehouseFilterSchema,
|
||||||
WarehouseFilterType,
|
WarehouseFilterType,
|
||||||
@@ -120,9 +112,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WarehousesTable = () => {
|
const WarehousesTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -141,6 +130,8 @@ const WarehousesTable = () => {
|
|||||||
areaFilter: 'area_id',
|
areaFilter: 'area_id',
|
||||||
activeProjectFlockFilter: 'active_project_flock',
|
activeProjectFlockFilter: 'active_project_flock',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'warehouses-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -149,27 +140,36 @@ const WarehousesTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<WarehouseFilterType>({
|
const formik = useFormik<WarehouseFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: null,
|
area_id: tableFilterState.areaFilter || null,
|
||||||
active_project_flock: false,
|
active_project_flock:
|
||||||
|
tableFilterState.activeProjectFlockFilter === 'true',
|
||||||
},
|
},
|
||||||
validationSchema: WarehouseFilterSchema,
|
validationSchema: WarehouseFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id || '');
|
updateFilter('areaFilter', values.area_id || '', true);
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'activeProjectFlockFilter',
|
'activeProjectFlockFilter',
|
||||||
values.active_project_flock === true ? 'true' : ''
|
values.active_project_flock === true ? 'true' : '',
|
||||||
|
true
|
||||||
);
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('areaFilter', '');
|
|
||||||
updateFilter('activeProjectFlockFilter', '');
|
|
||||||
formik.setFieldValue('active_project_flock', false);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setFieldValue } = formik;
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('areaFilter', '', true);
|
||||||
|
updateFilter('activeProjectFlockFilter', '', true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
area_id: null,
|
||||||
|
active_project_flock: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
// ===== AREA OPTIONS =====
|
// ===== AREA OPTIONS =====
|
||||||
const {
|
const {
|
||||||
@@ -243,26 +243,6 @@ const WarehousesTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (filterModal.open) {
|
|
||||||
const activeProjectFlockValue =
|
|
||||||
tableFilterState.activeProjectFlockFilter === 'true' ? true : false; // Default ke false (Semua Kandang)
|
|
||||||
setFieldValue('active_project_flock', activeProjectFlockValue);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
filterModal.open,
|
|
||||||
tableFilterState.activeProjectFlockFilter,
|
|
||||||
setFieldValue,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('warehouses-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -281,8 +261,7 @@ const WarehousesTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -507,7 +486,7 @@ const WarehousesTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
@@ -538,10 +517,7 @@ const WarehousesTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
onClick={formikResetHandler}
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useModal } from '@/components/Modal';
|
|||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Dropdown from '@/components/Dropdown';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
@@ -23,7 +22,6 @@ import { Icon } from '@iconify/react';
|
|||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -45,6 +43,7 @@ import {
|
|||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
@@ -59,8 +58,7 @@ const RowOptionsMenu = ({
|
|||||||
detailClickHandler: (id: number) => void;
|
detailClickHandler: (id: number) => void;
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
// TODO: change this to real condition
|
const showEditButton = props.row.original.approval?.step_number !== 2;
|
||||||
const showEditButton = true;
|
|
||||||
|
|
||||||
const showDeleteButton = showEditButton;
|
const showDeleteButton = showEditButton;
|
||||||
|
|
||||||
@@ -149,7 +147,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
||||||
@@ -175,6 +172,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
kandang_id: '',
|
kandang_id: '',
|
||||||
category: '',
|
category: '',
|
||||||
period: '',
|
period: '',
|
||||||
|
area_name: '',
|
||||||
|
location_name: '',
|
||||||
|
kandang_name: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -186,7 +186,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
category: 'category',
|
category: 'category',
|
||||||
period: 'period',
|
period: 'period',
|
||||||
},
|
},
|
||||||
|
excludeKeysFromUrl: ['area_name', 'location_name', 'kandang_name'],
|
||||||
|
persist: true,
|
||||||
|
storeName: 'project-flock-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// ===== State =====
|
// ===== State =====
|
||||||
@@ -207,8 +211,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
|
||||||
useState(false);
|
|
||||||
const {
|
const {
|
||||||
isChickinApproveModalOpen,
|
isChickinApproveModalOpen,
|
||||||
isChickinApproveLoading,
|
isChickinApproveLoading,
|
||||||
@@ -258,6 +261,18 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
updateFilter('kandang_id', values.kandang_id || '');
|
updateFilter('kandang_id', values.kandang_id || '');
|
||||||
updateFilter('category', values.category || '');
|
updateFilter('category', values.category || '');
|
||||||
updateFilter('period', values.period || '');
|
updateFilter('period', values.period || '');
|
||||||
|
updateFilter(
|
||||||
|
'area_name',
|
||||||
|
areaValue?.label ? String(areaValue.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'location_name',
|
||||||
|
locationValue?.label ? String(locationValue.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'kandang_name',
|
||||||
|
kandangValue?.label ? String(kandangValue.label) : ''
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
@@ -267,6 +282,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
updateFilter('kandang_id', '');
|
updateFilter('kandang_id', '');
|
||||||
updateFilter('category', '');
|
updateFilter('category', '');
|
||||||
updateFilter('period', '');
|
updateFilter('period', '');
|
||||||
|
updateFilter('area_name', '');
|
||||||
|
updateFilter('location_name', '');
|
||||||
|
updateFilter('kandang_name', '');
|
||||||
setFilterAreaId(undefined);
|
setFilterAreaId(undefined);
|
||||||
setFilterLocationId(undefined);
|
setFilterLocationId(undefined);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
@@ -308,40 +326,55 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const periodOptions = useMemo(
|
|
||||||
() => [
|
|
||||||
{ value: '1', label: 'Periode 1' },
|
|
||||||
{ value: '2', label: 'Periode 2' },
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
// ===== FILTER HELPERS =====
|
||||||
const areaValue = useMemo(() => {
|
const areaValue = useMemo(() => {
|
||||||
if (!formik.values.area_id) return null;
|
if (!formik.values.area_id) return null;
|
||||||
return (
|
const found = areaOptions.find(
|
||||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
(opt) => String(opt.value) === formik.values.area_id
|
||||||
null
|
|
||||||
);
|
);
|
||||||
}, [formik.values.area_id, areaOptions]);
|
if (found) return found;
|
||||||
|
if (tableFilterState.area_name) {
|
||||||
|
return {
|
||||||
|
value: formik.values.area_id,
|
||||||
|
label: tableFilterState.area_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [formik.values.area_id, areaOptions, tableFilterState.area_name]);
|
||||||
|
|
||||||
const locationValue = useMemo(() => {
|
const locationValue = useMemo(() => {
|
||||||
if (!formik.values.location_id) return null;
|
if (!formik.values.location_id) return null;
|
||||||
return (
|
const found = locationOptions.find(
|
||||||
locationOptions.find(
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
|
||||||
) || null
|
|
||||||
);
|
);
|
||||||
}, [formik.values.location_id, locationOptions]);
|
if (found) return found;
|
||||||
|
if (tableFilterState.location_name) {
|
||||||
|
return {
|
||||||
|
value: formik.values.location_id,
|
||||||
|
label: tableFilterState.location_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [
|
||||||
|
formik.values.location_id,
|
||||||
|
locationOptions,
|
||||||
|
tableFilterState.location_name,
|
||||||
|
]);
|
||||||
|
|
||||||
const kandangValue = useMemo(() => {
|
const kandangValue = useMemo(() => {
|
||||||
if (!formik.values.kandang_id) return null;
|
if (!formik.values.kandang_id) return null;
|
||||||
return (
|
const found = kandangOptions.find(
|
||||||
kandangOptions.find(
|
(opt) => String(opt.value) === formik.values.kandang_id
|
||||||
(opt) => String(opt.value) === formik.values.kandang_id
|
|
||||||
) || null
|
|
||||||
);
|
);
|
||||||
}, [formik.values.kandang_id, kandangOptions]);
|
if (found) return found;
|
||||||
|
if (tableFilterState.kandang_name) {
|
||||||
|
return {
|
||||||
|
value: formik.values.kandang_id,
|
||||||
|
label: tableFilterState.kandang_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [formik.values.kandang_id, kandangOptions, tableFilterState.kandang_name]);
|
||||||
|
|
||||||
const categoryValue = useMemo(() => {
|
const categoryValue = useMemo(() => {
|
||||||
if (!formik.values.category) return null;
|
if (!formik.values.category) return null;
|
||||||
@@ -351,13 +384,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.category, categoryOptions]);
|
}, [formik.values.category, categoryOptions]);
|
||||||
|
|
||||||
const periodValue = useMemo(() => {
|
|
||||||
if (!formik.values.period) return null;
|
|
||||||
return (
|
|
||||||
periodOptions.find((opt) => opt.value === formik.values.period) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.period, periodOptions]);
|
|
||||||
|
|
||||||
// ===== FILTER DEPENDENCY HANDLERS =====
|
// ===== FILTER DEPENDENCY HANDLERS =====
|
||||||
const handleFilterAreaChange = (area: OptionType | null) => {
|
const handleFilterAreaChange = (area: OptionType | null) => {
|
||||||
const areaId = area?.value ? String(area.value) : undefined;
|
const areaId = area?.value ? String(area.value) : undefined;
|
||||||
@@ -426,18 +452,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('project-flock-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmApprovalHandler = async (
|
const confirmApprovalHandler = async (
|
||||||
notes: string,
|
notes: string,
|
||||||
approvalAction: 'APPROVED' | 'REJECTED'
|
approvalAction: 'APPROVED' | 'REJECTED'
|
||||||
@@ -555,6 +574,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
price: budget.price,
|
price: budget.price,
|
||||||
total_price: budget.qty * budget.price,
|
total_price: budget.qty * budget.price,
|
||||||
})) || [],
|
})) || [],
|
||||||
|
periode: createdProjectFlock.period ?? '-',
|
||||||
} as ProjectFlockFormValues;
|
} as ProjectFlockFormValues;
|
||||||
}, [createdProjectFlock]);
|
}, [createdProjectFlock]);
|
||||||
|
|
||||||
@@ -777,14 +797,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const exportToExcelHandler = async () => {
|
|
||||||
setIsLoadingExportingToExcel(true);
|
|
||||||
|
|
||||||
toast.error('Not implemented yet!');
|
|
||||||
|
|
||||||
setIsLoadingExportingToExcel(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const bulkApproveClickHandler = () => {
|
const bulkApproveClickHandler = () => {
|
||||||
setApprovalAction('APPROVED');
|
setApprovalAction('APPROVED');
|
||||||
confirmModal.openModal();
|
confirmModal.openModal();
|
||||||
@@ -973,55 +985,17 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
|
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={['page', 'pageSize', 'search']}
|
excludeFields={[
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'search',
|
||||||
|
'area_name',
|
||||||
|
'location_name',
|
||||||
|
'kandang_name',
|
||||||
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
align='end'
|
|
||||||
direction='bottom'
|
|
||||||
className={{
|
|
||||||
content:
|
|
||||||
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
|
||||||
}}
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='none'
|
|
||||||
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
|
||||||
>
|
|
||||||
<div className='flex flex-row items-center gap-1.5'>
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:cloud-arrow-down'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span>Export</span>
|
|
||||||
|
|
||||||
<div className='w-px self-stretch bg-base-content/10' />
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:chevron-down'
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={exportToExcelHandler}
|
|
||||||
isLoading={isLoadingExportingToExcel}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Export to Excel
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1350,18 +1324,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
isClearable={true}
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInputRadio
|
<NumberInput
|
||||||
label='Periode'
|
label='Periode'
|
||||||
placeholder='Pilih Periode'
|
name='period'
|
||||||
options={periodOptions}
|
placeholder='Masukkan Periode'
|
||||||
value={periodValue}
|
value={formik.values.period ?? ''}
|
||||||
onChange={(val) => {
|
onChange={formik.handleChange}
|
||||||
if (!Array.isArray(val)) {
|
onBlur={formik.handleBlur}
|
||||||
formik.setFieldValue('period', val?.value || null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isClearable
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type ProjectFlockFormSchemaType = {
|
|||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
location_id: number;
|
location_id: number;
|
||||||
|
periode: number | string;
|
||||||
kandang_ids: number[];
|
kandang_ids: number[];
|
||||||
project_budgets: ProjectFlockBudgetsSchemaType[];
|
project_budgets: ProjectFlockBudgetsSchemaType[];
|
||||||
};
|
};
|
||||||
@@ -109,6 +110,12 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType
|
|||||||
.min(1, 'Lokasi wajib diisi!')
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
.required('Lokasi wajib diisi!'),
|
.required('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
|
// Period
|
||||||
|
periode: Yup.number()
|
||||||
|
.typeError('Periode harus berupa angka!')
|
||||||
|
.min(1, 'Periode minimal 1!')
|
||||||
|
.required('Periode wajib diisi!'),
|
||||||
|
|
||||||
kandang_ids: Yup.array()
|
kandang_ids: Yup.array()
|
||||||
.of(Yup.number().required('Kandang tidak valid!'))
|
.of(Yup.number().required('Kandang tidak valid!'))
|
||||||
.min(1, 'Minimal harus ada 1 kandang!')
|
.min(1, 'Minimal harus ada 1 kandang!')
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ export const ProjectFlockFormConfirmationTable = ({
|
|||||||
label: 'Standar Produksi',
|
label: 'Standar Produksi',
|
||||||
value: projectFlockForm?.production_standard?.label ?? '-',
|
value: projectFlockForm?.production_standard?.label ?? '-',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Periode',
|
||||||
|
value: projectFlockForm?.periode ?? '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Informasi Kandang',
|
label: 'Informasi Kandang',
|
||||||
value: '',
|
value: '',
|
||||||
@@ -261,7 +265,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingFlocks,
|
isLoadingOptions: isLoadingFlocks,
|
||||||
options: optionsFlock,
|
options: optionsFlock,
|
||||||
loadMore: loadMoreFlock,
|
loadMore: loadMoreFlock,
|
||||||
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
} = useSelect(FlockApi.basePath, 'id', 'name', 'search', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
location_id: selectedLocation,
|
location_id: selectedLocation,
|
||||||
area_id: selectedArea,
|
area_id: selectedArea,
|
||||||
@@ -279,7 +283,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
setInputValue: setInputValueLocation,
|
setInputValue: setInputValueLocation,
|
||||||
loadMore: loadMoreLocation,
|
loadMore: loadMoreLocation,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
area_id:
|
area_id:
|
||||||
selectedArea != ''
|
selectedArea != ''
|
||||||
? selectedArea
|
? selectedArea
|
||||||
@@ -291,7 +295,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingProductionStandards,
|
isLoadingOptions: isLoadingProductionStandards,
|
||||||
setInputValue: setInputValueProductionStandard,
|
setInputValue: setInputValueProductionStandard,
|
||||||
loadMore: loadMoreProductionStandard,
|
loadMore: loadMoreProductionStandard,
|
||||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', 'search', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -307,7 +311,7 @@ const ProjectFlockForm = ({
|
|||||||
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
||||||
|
|
||||||
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
||||||
`${selectedFlock?.toString()}/periods`,
|
selectedFlock ? `${selectedFlock?.toString()}/periods` : undefined,
|
||||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -529,6 +533,7 @@ const ProjectFlockForm = ({
|
|||||||
kandang_ids: initialValues?.kandangs?.map(
|
kandang_ids: initialValues?.kandangs?.map(
|
||||||
(k: Kandang) => k.id
|
(k: Kandang) => k.id
|
||||||
) as number[],
|
) as number[],
|
||||||
|
periode: initialValues?.period ?? '',
|
||||||
project_budgets: initialValues?.project_budgets?.map((budget) => {
|
project_budgets: initialValues?.project_budgets?.map((budget) => {
|
||||||
return {
|
return {
|
||||||
nonstock: {
|
nonstock: {
|
||||||
@@ -568,6 +573,7 @@ const ProjectFlockForm = ({
|
|||||||
category: values.category as string,
|
category: values.category as string,
|
||||||
production_standard_id: values.production_standard_id as number,
|
production_standard_id: values.production_standard_id as number,
|
||||||
location_id: values.location_id as number,
|
location_id: values.location_id as number,
|
||||||
|
periode: parseInt(values.periode as unknown as string),
|
||||||
kandang_ids: values.kandang_ids as number[],
|
kandang_ids: values.kandang_ids as number[],
|
||||||
project_budgets: values.project_budgets.flatMap((budget) => {
|
project_budgets: values.project_budgets.flatMap((budget) => {
|
||||||
return {
|
return {
|
||||||
@@ -793,6 +799,7 @@ const ProjectFlockForm = ({
|
|||||||
formik.values.kandang_ids?.includes(kandang.id)
|
formik.values.kandang_ids?.includes(kandang.id)
|
||||||
)?.period
|
)?.period
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const inputPeriod =
|
const inputPeriod =
|
||||||
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
||||||
|
|
||||||
@@ -1022,12 +1029,18 @@ const ProjectFlockForm = ({
|
|||||||
isDisabled={formType != 'add'}
|
isDisabled={formType != 'add'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
name='period'
|
name='periode'
|
||||||
label='Periode'
|
label='Periode'
|
||||||
disabled
|
|
||||||
readOnly
|
|
||||||
placeholder='Periode Flock'
|
placeholder='Periode Flock'
|
||||||
value={selectedLocation ? inputPeriod : ''}
|
value={formik.values.periode}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
allowNegative={false}
|
||||||
|
decimalScale={0}
|
||||||
|
isError={
|
||||||
|
formik.touched.periode && Boolean(formik.errors.periode)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.periode as string}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, {
|
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
||||||
useCallback,
|
|
||||||
useState,
|
|
||||||
useMemo,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
} from 'react';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
@@ -18,9 +12,11 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
@@ -36,16 +32,16 @@ import {
|
|||||||
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
|
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { type Recording } from '@/types/api/production/recording';
|
import { type Recording } from '@/types/api/production/recording';
|
||||||
|
import { getRecordingRestriction } from './recording-utils';
|
||||||
import { RecordingApi } from '@/services/api/production';
|
import { RecordingApi } from '@/services/api/production';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
|
||||||
// ===== STATUS BADGE UTILITIES =====
|
// ===== STATUS BADGE UTILITIES =====
|
||||||
const statusTextMap: Record<string, string> = {
|
const statusTextMap: Record<string, string> = {
|
||||||
@@ -72,6 +68,26 @@ const getStatusBadgeColor = (status: string): Color => {
|
|||||||
return statusBadgeColorMap[normalizedStatus] || 'neutral';
|
return statusBadgeColorMap[normalizedStatus] || 'neutral';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRecordingApproved = (recording: Recording): boolean => {
|
||||||
|
return (
|
||||||
|
recording.approval?.action === 'APPROVED' &&
|
||||||
|
recording.approval?.step_name === 'Disetujui'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const recordingApprovalStatusOptions: OptionType<string>[] = [
|
||||||
|
{ value: 'CREATED', label: 'Pengajuan' },
|
||||||
|
{ value: 'UPDATED', label: 'Diperbarui' },
|
||||||
|
{ value: 'APPROVED', label: 'Disetujui' },
|
||||||
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const projectFlockCategoryOptions: OptionType<string>[] = [
|
||||||
|
{ value: 'GROWING', label: 'Growing' },
|
||||||
|
{ value: 'LAYING', label: 'Laying' },
|
||||||
|
];
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
@@ -105,30 +121,75 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isRecordingEditable = (recording: Recording) => {
|
const isRecordingEditable = (recording: Recording) => {
|
||||||
if (
|
const isGrowingCategory =
|
||||||
recording.executed_at &&
|
recording.project_flock?.project_flock_category === 'GROWING';
|
||||||
recording.project_flock?.project_flock_category === 'GROWING'
|
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||||
) {
|
if (isGrowingLockedByLaying) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIsLaying =
|
||||||
|
recording.project_flock?.project_flock_category === 'LAYING';
|
||||||
|
|
||||||
|
const restriction = getRecordingRestriction(
|
||||||
|
recording.is_laying,
|
||||||
|
recording.is_transition,
|
||||||
|
currentIsLaying
|
||||||
|
);
|
||||||
|
|
||||||
|
if (restriction.isLocked) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRecordingRestrictionInfo = (recording: Recording) => {
|
||||||
|
const isGrowingCategory =
|
||||||
|
recording.project_flock?.project_flock_category === 'GROWING';
|
||||||
|
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||||
|
if (isGrowingLockedByLaying) {
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason:
|
||||||
|
'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIsLaying =
|
||||||
|
recording.project_flock?.project_flock_category === 'LAYING';
|
||||||
|
|
||||||
|
return getRecordingRestriction(
|
||||||
|
recording.is_laying,
|
||||||
|
recording.is_transition,
|
||||||
|
currentIsLaying
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const isApproved = isRecordingApproved(props.row.original);
|
const isApproved = isRecordingApproved(props.row.original);
|
||||||
const isRejected = isRecordingRejected(props.row.original);
|
const isRejected = isRecordingRejected(props.row.original);
|
||||||
const isEditable = isRecordingEditable(props.row.original);
|
const isEditable = isRecordingEditable(props.row.original);
|
||||||
|
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<PopoverButton
|
<Tooltip
|
||||||
tabIndex={0}
|
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''}
|
||||||
variant='ghost'
|
position='top'
|
||||||
color='none'
|
|
||||||
popoverTarget={popoverId}
|
|
||||||
anchorName={popoverAnchorName}
|
|
||||||
>
|
>
|
||||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
<PopoverButton
|
||||||
</PopoverButton>
|
tabIndex={0}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
popoverTarget={popoverId}
|
||||||
|
anchorName={popoverAnchorName}
|
||||||
|
className={restrictionInfo.isLocked ? 'text-error' : ''}
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||||
|
</PopoverButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
id={popoverId}
|
id={popoverId}
|
||||||
@@ -218,80 +279,111 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RecordingTable = () => {
|
const RecordingTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
areaFilter: OptionType<number> | null;
|
||||||
|
locationFilter: OptionType<number> | null;
|
||||||
|
projectFlockFilter: OptionType<number> | null;
|
||||||
|
kandangFilter: OptionType<number> | null;
|
||||||
|
projectFlockKandangFilter: number | null;
|
||||||
|
approvalStatusFilter: OptionType<string> | null;
|
||||||
|
projectFlockCategoryFilter: OptionType<string> | null;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
areaFilter: '',
|
areaFilter: null,
|
||||||
locationFilter: '',
|
locationFilter: null,
|
||||||
kandangFilter: '',
|
projectFlockFilter: null,
|
||||||
projectFlockKandangFilter: '',
|
kandangFilter: null,
|
||||||
|
projectFlockKandangFilter: null,
|
||||||
|
approvalStatusFilter: null,
|
||||||
|
projectFlockCategoryFilter: null,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
search: 'search',
|
search: 'search',
|
||||||
|
areaFilter: 'area_id',
|
||||||
|
locationFilter: 'location_id',
|
||||||
|
projectFlockFilter: 'project_flock_id',
|
||||||
kandangFilter: 'kandang_id',
|
kandangFilter: 'kandang_id',
|
||||||
projectFlockKandangFilter: 'project_flock_kandang_id',
|
projectFlockKandangFilter: 'project_flock_kandang_id',
|
||||||
|
approvalStatusFilter: 'approval_status',
|
||||||
|
projectFlockCategoryFilter: 'project_flock_category',
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
persist: true,
|
||||||
updateFilter('search', searchValue);
|
storeName: 'recording-table',
|
||||||
}, [searchValue, updateFilter]);
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
// ===== FILTER STATE =====
|
|
||||||
const [filterArea, setFilterArea] = useState<OptionType | null>(null);
|
|
||||||
const [filterLocation, setFilterLocation] = useState<OptionType | null>(null);
|
|
||||||
const [filterProjectFlock, setFilterProjectFlock] =
|
|
||||||
useState<OptionType | null>(null);
|
|
||||||
const [filterKandang, setFilterKandang] = useState<OptionType | null>(null);
|
|
||||||
const [, setFilterProjectFlockKandangId] = useState<number | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [filterLocationAreaId, setFilterLocationAreaId] = useState<string>('');
|
|
||||||
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
|
|
||||||
useState<string>('');
|
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<RecordingFilterType>({
|
const formik = useFormik<RecordingFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: null,
|
area_id: tableFilterState.areaFilter,
|
||||||
location_id: null,
|
location_id: tableFilterState.locationFilter,
|
||||||
kandang_id: null,
|
project_flock_id: tableFilterState.projectFlockFilter,
|
||||||
project_flock_kandang_id: null,
|
kandang_id: tableFilterState.kandangFilter,
|
||||||
|
project_flock_kandang_id: tableFilterState.projectFlockKandangFilter,
|
||||||
|
approval_status: tableFilterState.approvalStatusFilter,
|
||||||
|
project_flock_category: tableFilterState.projectFlockCategoryFilter,
|
||||||
},
|
},
|
||||||
validationSchema: RecordingFilterSchema,
|
validationSchema: RecordingFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id || '');
|
updateFilter('areaFilter', values.area_id, true);
|
||||||
updateFilter('locationFilter', values.location_id || '');
|
updateFilter('locationFilter', values.location_id, true);
|
||||||
updateFilter('kandangFilter', values.kandang_id || '');
|
updateFilter('projectFlockFilter', values.project_flock_id, true);
|
||||||
|
updateFilter('kandangFilter', values.kandang_id, true);
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'projectFlockKandangFilter',
|
'projectFlockKandangFilter',
|
||||||
values.project_flock_kandang_id || ''
|
values.project_flock_kandang_id,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
updateFilter('approvalStatusFilter', values.approval_status, true);
|
||||||
|
updateFilter(
|
||||||
|
'projectFlockCategoryFilter',
|
||||||
|
values.project_flock_category,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('areaFilter', '');
|
|
||||||
updateFilter('locationFilter', '');
|
|
||||||
updateFilter('kandangFilter', '');
|
|
||||||
updateFilter('projectFlockKandangFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('areaFilter', null, true);
|
||||||
|
updateFilter('locationFilter', null, true);
|
||||||
|
updateFilter('projectFlockFilter', null, true);
|
||||||
|
updateFilter('kandangFilter', null, true);
|
||||||
|
updateFilter('projectFlockKandangFilter', null, true);
|
||||||
|
updateFilter('approvalStatusFilter', null, true);
|
||||||
|
updateFilter('projectFlockCategoryFilter', null, true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
area_id: null,
|
||||||
|
location_id: null,
|
||||||
|
project_flock_id: null,
|
||||||
|
kandang_id: null,
|
||||||
|
project_flock_kandang_id: null,
|
||||||
|
approval_status: null,
|
||||||
|
project_flock_category: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { project_flock_id, kandang_id } = formik.values;
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||||
@@ -305,9 +397,16 @@ const RecordingTable = () => {
|
|||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
|
useState(false);
|
||||||
|
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||||
|
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
||||||
|
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
||||||
|
|
||||||
const singleDeleteModal = useModal();
|
const singleDeleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const exportProgressInputModal = useModal();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: recordings,
|
data: recordings,
|
||||||
@@ -319,13 +418,6 @@ const RecordingTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== LOCATION, AREA, KANDANG OPTIONS =====
|
// ===== LOCATION, AREA, KANDANG OPTIONS =====
|
||||||
const locationParams = useMemo(() => {
|
|
||||||
if (filterLocationAreaId) {
|
|
||||||
return { area_id: filterLocationAreaId };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [filterLocationAreaId]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
@@ -336,7 +428,9 @@ const RecordingTable = () => {
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'search',
|
'search',
|
||||||
locationParams
|
{
|
||||||
|
area_id: String(formik.values.area_id?.value),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -351,13 +445,6 @@ const RecordingTable = () => {
|
|||||||
'search'
|
'search'
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectFlockParams = useMemo(() => {
|
|
||||||
if (filterProjectFlockLocationId) {
|
|
||||||
return { location_id: filterProjectFlockLocationId };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [filterProjectFlockLocationId]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setProjectFlockInputValue,
|
setInputValue: setProjectFlockInputValue,
|
||||||
options: projectFlockOptions,
|
options: projectFlockOptions,
|
||||||
@@ -369,34 +456,41 @@ const RecordingTable = () => {
|
|||||||
'id',
|
'id',
|
||||||
'flock_name',
|
'flock_name',
|
||||||
'search',
|
'search',
|
||||||
projectFlockParams
|
{
|
||||||
|
location_id: String(formik.values.location_id?.value),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const kandangOptions = useMemo(() => {
|
const kandangOptions = useMemo(() => {
|
||||||
if (!filterProjectFlock || !projectFlocksRawData) return [];
|
if (!project_flock_id || !projectFlocksRawData) return [];
|
||||||
if (!isResponseSuccess(projectFlocksRawData)) return [];
|
if (!isResponseSuccess(projectFlocksRawData)) return [];
|
||||||
|
|
||||||
const data = projectFlocksRawData.data as ProjectFlock[];
|
const data = projectFlocksRawData.data as ProjectFlock[];
|
||||||
const selectedProjectFlockData = data.find(
|
const selectedProjectFlockData = data.find((pf) =>
|
||||||
(pf) => pf.id === filterProjectFlock.value
|
pf.id === formik.values.project_flock_id?.value
|
||||||
|
? Number(formik.values.project_flock_id.value)
|
||||||
|
: 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!selectedProjectFlockData?.kandangs) return [];
|
if (!selectedProjectFlockData?.kandangs) return [];
|
||||||
|
|
||||||
return selectedProjectFlockData.kandangs.map((k) => ({
|
return selectedProjectFlockData.kandangs.map((k) => ({
|
||||||
value: k.id,
|
value: k.id,
|
||||||
label: k.name || '',
|
label: k.name || '',
|
||||||
}));
|
}));
|
||||||
}, [filterProjectFlock, projectFlocksRawData]);
|
}, [project_flock_id, projectFlocksRawData]);
|
||||||
|
|
||||||
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
||||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||||
if (!filterProjectFlock || !filterKandang) return null;
|
if (!project_flock_id?.value || !kandang_id?.value) return null;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
project_flock_id: filterProjectFlock.value.toString(),
|
project_flock_id: project_flock_id.value.toString(),
|
||||||
kandang_id: filterKandang.value.toString(),
|
kandang_id: kandang_id.value.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
||||||
}, [filterProjectFlock, filterKandang]);
|
}, [project_flock_id, kandang_id]);
|
||||||
|
|
||||||
const { data: projectFlockKandangLookupData } = useSWR(
|
const { data: projectFlockKandangLookupData } = useSWR(
|
||||||
projectFlockKandangLookupUrl,
|
projectFlockKandangLookupUrl,
|
||||||
@@ -418,118 +512,45 @@ const RecordingTable = () => {
|
|||||||
? projectFlockKandangLookupData.data
|
? projectFlockKandangLookupData.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const formikRef = useRef(formik);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
formikRef.current = formik;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectFlockKandangLookup?.id) {
|
if (projectFlockKandangLookup?.id) {
|
||||||
const pfkId = String(projectFlockKandangLookup.id);
|
const pfkId = String(projectFlockKandangLookup.id);
|
||||||
setFilterProjectFlockKandangId(projectFlockKandangLookup.id);
|
formik.setFieldValue('project_flock_kandang_id', pfkId);
|
||||||
formikRef.current.setFieldValue('project_flock_kandang_id', pfkId);
|
|
||||||
} else {
|
} else {
|
||||||
setFilterProjectFlockKandangId(undefined);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
formikRef.current.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
}
|
}
|
||||||
}, [projectFlockKandangLookup]);
|
}, [projectFlockKandangLookup]);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterAreaChange = useCallback(
|
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('area_id', val);
|
||||||
const area = val as OptionType | null;
|
formik.setFieldValue('location_id', null);
|
||||||
const areaId = area?.value ? String(area.value) : null;
|
formik.setFieldValue('project_flock_id', null);
|
||||||
|
formik.setFieldValue('kandang_id', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
|
};
|
||||||
|
|
||||||
formik.setFieldValue('area_id', areaId);
|
const handleFilterLocationChange = (
|
||||||
formik.setFieldValue('location_id', null);
|
val: OptionType | OptionType[] | null
|
||||||
formik.setFieldValue('kandang_id', null);
|
) => {
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('location_id', val);
|
||||||
|
formik.setFieldValue('project_flock_id', null);
|
||||||
|
formik.setFieldValue('kandang_id', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
|
};
|
||||||
|
|
||||||
setFilterArea(area);
|
const handleFilterProjectFlockChange = (
|
||||||
setFilterLocation(null);
|
val: OptionType | OptionType[] | null
|
||||||
setFilterProjectFlock(null);
|
) => {
|
||||||
setFilterKandang(null);
|
formik.setFieldValue('project_flock_id', val);
|
||||||
setFilterLocationAreaId(areaId || '');
|
formik.setFieldValue('kandang_id', null);
|
||||||
setFilterProjectFlockLocationId('');
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
},
|
};
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterLocationChange = useCallback(
|
const handleFilterKandangChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('kandang_id', val);
|
||||||
const location = val as OptionType | null;
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
const locationId = location?.value ? String(location.value) : null;
|
};
|
||||||
|
|
||||||
formik.setFieldValue('location_id', locationId);
|
|
||||||
formik.setFieldValue('kandang_id', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
|
|
||||||
setFilterLocation(location);
|
|
||||||
setFilterProjectFlock(null);
|
|
||||||
setFilterKandang(null);
|
|
||||||
setFilterProjectFlockLocationId(locationId || '');
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterProjectFlockChange = useCallback(
|
|
||||||
(val: OptionType | OptionType[] | null) => {
|
|
||||||
const projectFlock = val as OptionType | null;
|
|
||||||
|
|
||||||
formik.setFieldValue('kandang_id', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
|
|
||||||
setFilterProjectFlock(projectFlock);
|
|
||||||
setFilterKandang(null);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterKandangChange = useCallback(
|
|
||||||
(val: OptionType | OptionType[] | null) => {
|
|
||||||
const kandang = val as OptionType | null;
|
|
||||||
const kandangId = kandang?.value ? String(kandang.value) : null;
|
|
||||||
|
|
||||||
formik.setFieldValue('kandang_id', kandangId);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
|
|
||||||
setFilterKandang(kandang);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const areaIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.area_id) return null;
|
|
||||||
return (
|
|
||||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}, [formik.values.area_id, areaOptions]);
|
|
||||||
|
|
||||||
const locationIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.location_id) return null;
|
|
||||||
return (
|
|
||||||
locationOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.location_id, locationOptions]);
|
|
||||||
|
|
||||||
const projectFlockIdValue = useMemo(() => {
|
|
||||||
if (!filterProjectFlock) return null;
|
|
||||||
return filterProjectFlock;
|
|
||||||
}, [filterProjectFlock]);
|
|
||||||
|
|
||||||
const kandangIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.kandang_id) return null;
|
|
||||||
return (
|
|
||||||
kandangOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.kandang_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.kandang_id, kandangOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -537,35 +558,24 @@ const RecordingTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRecordingApproved = useCallback((recording: Recording): boolean => {
|
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
return (
|
updateFilter('search', e.target.value, true);
|
||||||
recording.approval?.action === 'APPROVED' &&
|
};
|
||||||
recording.approval?.step_name === 'Disetujui'
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('recording-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
setSearchValue(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
},
|
|
||||||
[updateFilter, setSearchValue, setPage]
|
|
||||||
);
|
|
||||||
|
|
||||||
const singleDeleteHandler = async () => {
|
const singleDeleteHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await RecordingApi.delete(selectedRecording?.id as number);
|
const response = await RecordingApi.delete(selectedRecording?.id as number);
|
||||||
refreshRecordings();
|
|
||||||
|
|
||||||
singleDeleteModal.closeModal();
|
singleDeleteModal.closeModal();
|
||||||
toast.success('Successfully delete Recording!');
|
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
|
||||||
|
if (isResponseSuccess(response)) {
|
||||||
|
toast.success(response?.message || 'Successfully delete Recording!');
|
||||||
|
refreshRecordings();
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message || 'Failed to delete Recording');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveHandler = async (notes: string) => {
|
const approveHandler = async (notes: string) => {
|
||||||
@@ -634,6 +644,68 @@ const RecordingTable = () => {
|
|||||||
});
|
});
|
||||||
}, [selectedRowIds, recordings, isRecordingApproved]);
|
}, [selectedRowIds, recordings, isRecordingApproved]);
|
||||||
|
|
||||||
|
const exportToExcelHandler = async () => {
|
||||||
|
setIsLoadingExportingToExcel(true);
|
||||||
|
|
||||||
|
await RecordingApi.exportToExcel(getTableFilterQueryString());
|
||||||
|
|
||||||
|
setIsLoadingExportingToExcel(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetExportProgressForm = useCallback(() => {
|
||||||
|
setExportProgressStartDate('');
|
||||||
|
setExportProgressEndDate('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exportProgressStartDateChangeHandler = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setExportProgressStartDate(e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportProgressEndDateChangeHandler = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setExportProgressEndDate(e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportProgressInputToExcelClickHandler = useCallback(() => {
|
||||||
|
resetExportProgressForm();
|
||||||
|
exportProgressInputModal.openModal();
|
||||||
|
}, [exportProgressInputModal, resetExportProgressForm]);
|
||||||
|
|
||||||
|
const submitExportProgressInputHandler = useCallback(async () => {
|
||||||
|
if (!exportProgressStartDate || !exportProgressEndDate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExportProgressLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await RecordingApi.exportInputProgressToExcel(
|
||||||
|
exportProgressStartDate,
|
||||||
|
exportProgressEndDate
|
||||||
|
);
|
||||||
|
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
toast.success('Ekspor berhasil');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsExportProgressLoading(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
exportProgressEndDate,
|
||||||
|
exportProgressInputModal,
|
||||||
|
exportProgressStartDate,
|
||||||
|
resetExportProgressForm,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(recordings) && recordings.data) {
|
if (isResponseSuccess(recordings) && recordings.data) {
|
||||||
const newSelection: Record<string, boolean> = {};
|
const newSelection: Record<string, boolean> = {};
|
||||||
@@ -761,11 +833,30 @@ const RecordingTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
|
const isTransition = props.row.original.is_transition;
|
||||||
const category =
|
const category =
|
||||||
props.row.original.project_flock?.project_flock_category;
|
props.row.original.project_flock?.project_flock_category ||
|
||||||
if (!category) return '-';
|
'GROWING';
|
||||||
const color = category === 'LAYING' ? 'info' : 'warning';
|
const color = category === 'LAYING' ? 'info' : 'warning';
|
||||||
return <StatusBadge color={color} text={formatTitleCase(category)} />;
|
|
||||||
|
const isGrowingLocked =
|
||||||
|
category === 'GROWING' && props.row.original.is_laying;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<StatusBadge color={color} text={formatTitleCase(category)} />
|
||||||
|
{isTransition && (
|
||||||
|
<span className='text-xs text-warning font-medium'>
|
||||||
|
(Transisi)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isGrowingLocked && (
|
||||||
|
<span className='text-xs text-error font-medium'>
|
||||||
|
(Penguncian)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -775,7 +866,8 @@ const RecordingTable = () => {
|
|||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
{props.row.original.day} (Minggu ke-
|
{props.row.original.day} (Minggu ke-
|
||||||
{props.row.original.project_flock.production_standart.week})
|
{props.row.original.week} hari ke-
|
||||||
|
{props.row.original.excess_days})
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1021,7 +1113,7 @@ const RecordingTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
{value !== null && value !== undefined
|
{value !== null && value !== undefined
|
||||||
? `${value.toFixed(2)}%`
|
? `${value.toFixed(2)} butir`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1037,7 +1129,7 @@ const RecordingTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='text-center text-gray-600'>
|
<div className='text-center text-gray-600'>
|
||||||
{value !== null && value !== undefined
|
{value !== null && value !== undefined
|
||||||
? `${value.toFixed(2)}%`
|
? `${value.toFixed(2)} btr`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1242,6 +1334,60 @@ const RecordingTable = () => {
|
|||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
align='end'
|
||||||
|
direction='bottom'
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-2.5 rounded-lg font-semibold text-sm gap-1.5',
|
||||||
|
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
icon='heroicons:cloud-arrow-down'
|
||||||
|
/>
|
||||||
|
Export
|
||||||
|
<div className='w-6.5 h-5 flex items-center justify-center border-l border-base-content/10'>
|
||||||
|
<Icon
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
icon='heroicons:chevron-down'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
content:
|
||||||
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportToExcelHandler}
|
||||||
|
isLoading={isLoadingExportingToExcel}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Export to Excel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportProgressInputToExcelClickHandler}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Ekspor Input Progress (Excel)
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1319,13 +1465,13 @@ const RecordingTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={areaIdValue}
|
value={formik.values.area_id}
|
||||||
onChange={handleFilterAreaChange}
|
onChange={handleFilterAreaChange}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
isLoading={isLoadingAreaOptions}
|
isLoading={isLoadingAreaOptions}
|
||||||
@@ -1338,13 +1484,13 @@ const RecordingTable = () => {
|
|||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={locationIdValue}
|
value={formik.values.location_id}
|
||||||
onChange={handleFilterLocationChange}
|
onChange={handleFilterLocationChange}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isDisabled={!filterArea}
|
isDisabled={!formik.values.area_id?.value}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1352,13 +1498,13 @@ const RecordingTable = () => {
|
|||||||
label='Project Flock'
|
label='Project Flock'
|
||||||
placeholder='Pilih Project Flock'
|
placeholder='Pilih Project Flock'
|
||||||
options={projectFlockOptions}
|
options={projectFlockOptions}
|
||||||
value={projectFlockIdValue}
|
value={formik.values.project_flock_id}
|
||||||
onChange={handleFilterProjectFlockChange}
|
onChange={handleFilterProjectFlockChange}
|
||||||
onInputChange={setProjectFlockInputValue}
|
onInputChange={setProjectFlockInputValue}
|
||||||
isLoading={isLoadingProjectFlocks}
|
isLoading={isLoadingProjectFlocks}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isDisabled={!filterLocation}
|
isDisabled={!formik.values.location_id?.value}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1366,11 +1512,35 @@ const RecordingTable = () => {
|
|||||||
label='Kandang'
|
label='Kandang'
|
||||||
placeholder='Pilih Kandang'
|
placeholder='Pilih Kandang'
|
||||||
options={kandangOptions}
|
options={kandangOptions}
|
||||||
value={kandangIdValue}
|
value={formik.values.kandang_id}
|
||||||
onChange={handleFilterKandangChange}
|
onChange={handleFilterKandangChange}
|
||||||
isLoading={!filterProjectFlock}
|
isLoading={!formik.values.project_flock_id?.value}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.project_flock_id?.value}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={projectFlockCategoryOptions}
|
||||||
|
value={formik.values.project_flock_category}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue('project_flock_category', val);
|
||||||
|
}}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status Approval'
|
||||||
|
placeholder='Pilih Status Approval'
|
||||||
|
options={recordingApprovalStatusOptions}
|
||||||
|
value={formik.values.approval_status}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue('approval_status', val);
|
||||||
|
}}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!filterProjectFlock}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1378,30 +1548,16 @@ const RecordingTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
setFilterArea(null);
|
|
||||||
setFilterLocation(null);
|
|
||||||
setFilterProjectFlock(null);
|
|
||||||
setFilterKandang(null);
|
|
||||||
setFilterLocationAreaId('');
|
|
||||||
setFilterProjectFlockLocationId('');
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
!formik.isValid ||
|
|
||||||
formik.isSubmitting ||
|
|
||||||
!formik.values.kandang_id
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1424,6 +1580,76 @@ const RecordingTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
ref={exportProgressInputModal.ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'max-w-lg rounded-lg p-0',
|
||||||
|
}}
|
||||||
|
closeOnBackdrop
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
|
Ekspor Input Progress
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_start_date'
|
||||||
|
label='Tanggal Mulai'
|
||||||
|
value={exportProgressStartDate}
|
||||||
|
onChange={exportProgressStartDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_end_date'
|
||||||
|
label='Tanggal Selesai'
|
||||||
|
value={exportProgressEndDate}
|
||||||
|
onChange={exportProgressEndDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='success'
|
||||||
|
onClick={submitExportProgressInputHandler}
|
||||||
|
isLoading={isExportProgressLoading}
|
||||||
|
disabled={!exportProgressStartDate || !exportProgressEndDate}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={approveModal.ref}
|
ref={approveModal.ref}
|
||||||
type='success'
|
type='success'
|
||||||
|
|||||||
@@ -1,15 +1,40 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const RecordingFilterSchema = object().shape({
|
export const RecordingFilterSchema = Yup.object().shape({
|
||||||
area_id: string().nullable(),
|
area_id: Yup.object({
|
||||||
location_id: string().nullable(),
|
value: Yup.number().nullable(),
|
||||||
kandang_id: string().nullable(),
|
label: Yup.string().nullable(),
|
||||||
project_flock_kandang_id: string().nullable(),
|
}).nullable(),
|
||||||
|
location_id: Yup.object({
|
||||||
|
value: Yup.number().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_id: Yup.object({
|
||||||
|
value: Yup.number().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
kandang_id: Yup.object({
|
||||||
|
value: Yup.number().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_kandang_id: Yup.number().nullable(),
|
||||||
|
approval_status: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_category: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RecordingFilterType = {
|
export type RecordingFilterType = {
|
||||||
area_id: string | null;
|
area_id: OptionType<number> | null;
|
||||||
location_id: string | null;
|
location_id: OptionType<number> | null;
|
||||||
kandang_id: string | null;
|
project_flock_id: OptionType<number> | null;
|
||||||
project_flock_kandang_id: string | null;
|
kandang_id: OptionType<number> | null;
|
||||||
|
project_flock_kandang_id: number | null;
|
||||||
|
approval_status: OptionType<string> | null;
|
||||||
|
project_flock_category: OptionType<string> | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import {
|
|||||||
CreateGrowingRecordingPayload,
|
CreateGrowingRecordingPayload,
|
||||||
CreateLayingRecordingPayload,
|
CreateLayingRecordingPayload,
|
||||||
CreateEggPayload,
|
CreateEggPayload,
|
||||||
|
RecordingStock,
|
||||||
} from '@/types/api/production/recording';
|
} from '@/types/api/production/recording';
|
||||||
|
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
|
||||||
|
|
||||||
type RecordingGrowingFormSchemaType = {
|
type RecordingGrowingFormSchemaType = {
|
||||||
record_date: string;
|
record_date: string;
|
||||||
@@ -29,63 +31,96 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
} | null;
|
} | null;
|
||||||
project_flock_kandang_id: number;
|
project_flock_kandang_id: number;
|
||||||
stocks: {
|
stocks: {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id:
|
||||||
|
| {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
qty: number | string;
|
qty: number | string;
|
||||||
}[];
|
}[];
|
||||||
depletions: {
|
depletions: {
|
||||||
product_warehouse_id?: number;
|
product_warehouse_id?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
source_product_warehouse_id?: number;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
||||||
eggs: {
|
eggs: {
|
||||||
product_warehouse_id?: number;
|
product_warehouse_id?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
weight?: number | string;
|
weight?: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StockSchema = {
|
export type StockSchema = {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
qty: number | string;
|
qty: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DepletionSchema = {
|
export type DepletionSchema = {
|
||||||
product_warehouse_id?: number;
|
product_warehouse_id?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
source_product_warehouse_id?: number;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EggSchema = {
|
export type EggSchema = {
|
||||||
product_warehouse_id?: number;
|
product_warehouse_id?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
weight?: number | string;
|
weight?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
})
|
||||||
.required('Produk wajib diisi!')
|
.required('Produk wajib diisi!')
|
||||||
.min(1, 'Produk wajib diisi!')
|
.typeError('Produk wajib diisi!'),
|
||||||
.typeError('Produk harus berupa angka!'),
|
|
||||||
qty: Yup.number()
|
qty: Yup.number()
|
||||||
.required('Jumlah penggunaan wajib diisi!')
|
.required('Jumlah penggunaan wajib diisi!')
|
||||||
.min(1, 'Jumlah penggunaan tidak boleh 0!')
|
.moreThan(0, 'Jumlah penggunaan harus lebih dari 0!')
|
||||||
.typeError('Jumlah penggunaan harus berupa angka!'),
|
.typeError('Jumlah penggunaan harus berupa angka!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Depletions harus berupa angka!'),
|
.nullable(),
|
||||||
|
source_product_warehouse_id: Yup.number()
|
||||||
|
.optional()
|
||||||
|
.typeError('Gudang sumber harus berupa angka!'),
|
||||||
qty: Yup.number()
|
qty: Yup.number()
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Jumlah depletions harus berupa angka!'),
|
.typeError('Jumlah depletions harus berupa angka!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Kondisi telur harus berupa angka!'),
|
.nullable(),
|
||||||
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
||||||
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
||||||
});
|
});
|
||||||
@@ -243,14 +278,18 @@ export const getRecordingGrowingFormInitialValues = (
|
|||||||
initialValues?.project_flock?.project_flock_kandang_id ??
|
initialValues?.project_flock?.project_flock_kandang_id ??
|
||||||
0,
|
0,
|
||||||
stocks: initialValues?.stocks?.map((stock) => ({
|
stocks: initialValues?.stocks?.map((stock) => ({
|
||||||
product_warehouse_id: stock.product_warehouse_id,
|
product_warehouse_id: {
|
||||||
|
value: stock.product_warehouse_id,
|
||||||
|
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
||||||
|
},
|
||||||
qty:
|
qty:
|
||||||
(stock as { qty?: number; usage_amount?: number }).qty ||
|
(stock as RecordingStock).qty ||
|
||||||
(stock as { qty?: number; usage_amount?: number }).usage_amount ||
|
((stock as RecordingStock).usage_amount || 0) +
|
||||||
|
((stock as RecordingStock).pending_qty || 0) ||
|
||||||
'',
|
'',
|
||||||
})) ?? [
|
})) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: undefined,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -258,12 +297,16 @@ export const getRecordingGrowingFormInitialValues = (
|
|||||||
(
|
(
|
||||||
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
|
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
|
||||||
) => ({
|
) => ({
|
||||||
product_warehouse_id: depletion.product_warehouse_id,
|
product_warehouse_id: {
|
||||||
|
value: Number(depletion.product_warehouse_id ?? 0),
|
||||||
|
label: getProductWarehouseOptionLabel(depletion.product_warehouse),
|
||||||
|
},
|
||||||
|
source_product_warehouse_id: depletion.source_product_warehouse_id,
|
||||||
qty: depletion.qty,
|
qty: depletion.qty,
|
||||||
})
|
})
|
||||||
) ?? [
|
) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: undefined,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -275,12 +318,15 @@ export const getRecordingLayingFormInitialValues = (
|
|||||||
...getRecordingGrowingFormInitialValues(initialValues),
|
...getRecordingGrowingFormInitialValues(initialValues),
|
||||||
|
|
||||||
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
|
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
|
||||||
product_warehouse_id: egg.product_warehouse_id,
|
product_warehouse_id: {
|
||||||
|
value: Number(egg.product_warehouse_id ?? 0),
|
||||||
|
label: getProductWarehouseOptionLabel(egg.product_warehouse),
|
||||||
|
},
|
||||||
qty: egg.qty,
|
qty: egg.qty,
|
||||||
weight: egg.weight,
|
weight: egg.weight,
|
||||||
})) ?? [
|
})) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
|||||||
|
export type RecordingRestriction = {
|
||||||
|
canEditStock: boolean;
|
||||||
|
canEditDepletion: boolean;
|
||||||
|
canEditEgg: boolean;
|
||||||
|
isLocked: boolean;
|
||||||
|
lockReason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRecordingRestriction = (
|
||||||
|
isLaying: boolean,
|
||||||
|
isTransition: boolean,
|
||||||
|
currentIsLaying?: boolean
|
||||||
|
): RecordingRestriction => {
|
||||||
|
if (isTransition && !isLaying) {
|
||||||
|
const isLayingKandangInTransition = currentIsLaying === true;
|
||||||
|
|
||||||
|
if (isLayingKandangInTransition) {
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: true,
|
||||||
|
canEditEgg: true,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
canEditStock: true,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLaying && !isTransition && currentIsLaying) {
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason:
|
||||||
|
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLaying && !isTransition) {
|
||||||
|
return {
|
||||||
|
canEditStock: true,
|
||||||
|
canEditDepletion: true,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isLaying && !isTransition) {
|
||||||
|
return {
|
||||||
|
canEditStock: true,
|
||||||
|
canEditDepletion: true,
|
||||||
|
canEditEgg: true,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason: 'Kondisi transisi tidak valid',
|
||||||
|
};
|
||||||
|
};
|
||||||
+35
-23
@@ -40,6 +40,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
? transferToLayingResponse.data
|
? transferToLayingResponse.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isTransferToLayingApproved =
|
||||||
|
transferToLaying?.approval.step_number === 2;
|
||||||
|
|
||||||
const { data: transferToLayingApprovalResponse } = useSWR(
|
const { data: transferToLayingApprovalResponse } = useSWR(
|
||||||
transferToLayingId
|
transferToLayingId
|
||||||
? ['approval-transfer-to-laying', transferToLayingId]
|
? ['approval-transfer-to-laying', transferToLayingId]
|
||||||
@@ -55,9 +58,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
|
|
||||||
const detailModal = useModal();
|
const detailModal = useModal();
|
||||||
|
|
||||||
const totalEnteredChickenForTransfer =
|
const maxSourceQuantity =
|
||||||
transferToLaying?.sources.reduce(
|
transferToLaying?.sources.reduce(
|
||||||
(acc, item) => acc + Number(item.qty),
|
(acc, item) => acc + Number(item.product_warehouse.quantity),
|
||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
@@ -67,8 +70,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
|
// Sisa transfer = Max available dari kandang asal - Total yang sudah diisi di kandang tujuan
|
||||||
const totalAvailableChickenForTransfer =
|
const totalAvailableChickenForTransfer =
|
||||||
totalEnteredChickenForTransfer - totalTransferedChicken;
|
maxSourceQuantity - totalTransferedChicken;
|
||||||
|
|
||||||
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
||||||
if (shouldPushToRoute) {
|
if (shouldPushToRoute) {
|
||||||
@@ -161,11 +165,34 @@ const TransferToLayingDetailModal = () => {
|
|||||||
|
|
||||||
{/* Source Kandang */}
|
{/* Source Kandang */}
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<span className='w-full py-2 text-xs font-semibold'>
|
<span className='w-fit py-2 text-xs font-semibold flex flex-row items-center gap-3'>
|
||||||
Kandang Asal{' '}
|
<span className='text-nowrap'>
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
Kandang Asal{' '}
|
||||||
<span className='text-error'> *</span>
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
|
<span className='text-error'> *</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{!isTransferToLayingApproved && (
|
||||||
|
<>
|
||||||
|
<div className='w-px h-5 bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
totalAvailableChickenForTransfer < 0
|
||||||
|
? 'error'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
text={`Sisa ayam: ${formatNumber(
|
||||||
|
totalAvailableChickenForTransfer,
|
||||||
|
'en-US'
|
||||||
|
)} ekor`}
|
||||||
|
className={{
|
||||||
|
badge: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{transferToLaying?.sources.length === 0 && (
|
{transferToLaying?.sources.length === 0 && (
|
||||||
@@ -225,21 +252,6 @@ const TransferToLayingDetailModal = () => {
|
|||||||
<span className='text-error'> *</span>
|
<span className='text-error'> *</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='w-px h-5 bg-base-content/10' />
|
|
||||||
|
|
||||||
<StatusBadge
|
|
||||||
color={
|
|
||||||
totalAvailableChickenForTransfer < 0 ? 'error' : 'neutral'
|
|
||||||
}
|
|
||||||
text={`Sisa transfer: ${formatNumber(
|
|
||||||
totalAvailableChickenForTransfer,
|
|
||||||
'en-US'
|
|
||||||
)} ekor`}
|
|
||||||
className={{
|
|
||||||
badge: 'text-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{transferToLaying?.targets.length === 0 && (
|
{transferToLaying?.targets.length === 0 && (
|
||||||
@@ -304,7 +316,7 @@ const TransferToLayingDetailModal = () => {
|
|||||||
readOnly
|
readOnly
|
||||||
errorMessage={
|
errorMessage={
|
||||||
totalAvailableChickenForTransfer < 0
|
totalAvailableChickenForTransfer < 0
|
||||||
? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)`
|
? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+10
-21
@@ -13,7 +13,6 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
|||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { Flock } from '@/types/api/master-data/flock';
|
import { Flock } from '@/types/api/master-data/flock';
|
||||||
import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying';
|
|
||||||
import {
|
import {
|
||||||
TransferToLayingFilterSchema,
|
TransferToLayingFilterSchema,
|
||||||
TransferToLayingFilterValues,
|
TransferToLayingFilterValues,
|
||||||
@@ -21,12 +20,14 @@ import {
|
|||||||
|
|
||||||
interface TransferToLayingFilterModal {
|
interface TransferToLayingFilterModal {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
onSubmit?: (values: TransferToLayingFilter) => void;
|
initialValues?: Partial<TransferToLayingFilterValues>;
|
||||||
|
onSubmit?: (values: TransferToLayingFilterValues) => void;
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TransferToLayingFilterModal = ({
|
const TransferToLayingFilterModal = ({
|
||||||
ref,
|
ref,
|
||||||
|
initialValues: initialValuesProp,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: TransferToLayingFilterModal) => {
|
}: TransferToLayingFilterModal) => {
|
||||||
@@ -86,28 +87,16 @@ const TransferToLayingFilterModal = ({
|
|||||||
|
|
||||||
const formik = useFormik<TransferToLayingFilterValues>({
|
const formik = useFormik<TransferToLayingFilterValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
startDate: '',
|
startDate: initialValuesProp?.startDate ?? '',
|
||||||
endDate: '',
|
endDate: initialValuesProp?.endDate ?? '',
|
||||||
flockSource: [],
|
flockSource: initialValuesProp?.flockSource ?? [],
|
||||||
flockDestination: [],
|
flockDestination: initialValuesProp?.flockDestination ?? [],
|
||||||
status: [],
|
status: initialValuesProp?.status ?? [],
|
||||||
},
|
},
|
||||||
|
enableReinitialize: true,
|
||||||
validationSchema: TransferToLayingFilterSchema,
|
validationSchema: TransferToLayingFilterSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues = {
|
onSubmit?.(values);
|
||||||
...values,
|
|
||||||
flockSource: values.flockSource
|
|
||||||
? (values.flockSource as OptionType[]).map((item) => item.value)
|
|
||||||
: [],
|
|
||||||
flockDestination: values.flockDestination
|
|
||||||
? (values.flockDestination as OptionType[]).map((item) => item.value)
|
|
||||||
: [],
|
|
||||||
status: values.status
|
|
||||||
? (values.status as OptionType[]).map((item) => item.value)
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
onSubmit?.(formattedValues as TransferToLayingFilter);
|
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
|
|||||||
@@ -223,6 +223,8 @@ const TransferToLayingFormModal = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { flockSource: formikFlockSource } = formik.values;
|
||||||
|
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||||
|
|
||||||
const [selectedFlockSourceRawData, setSelectedFlockSourceRawData] = useState<
|
const [selectedFlockSourceRawData, setSelectedFlockSourceRawData] = useState<
|
||||||
@@ -455,13 +457,13 @@ const TransferToLayingFormModal = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(flockSourceRawData)) {
|
if (isResponseSuccess(flockSourceRawData)) {
|
||||||
const selectedFlockSourceRawData = flockSourceRawData.data.find(
|
const currentSelectedFlockSourceRawData = flockSourceRawData.data.find(
|
||||||
(item) => item.id === formik.values.flockSource?.value
|
(item) => item.id === formik.values.flockSource?.value
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedFlockSourceRawData(selectedFlockSourceRawData);
|
setSelectedFlockSourceRawData(currentSelectedFlockSourceRawData);
|
||||||
}
|
}
|
||||||
}, [flockSourceRawData]);
|
}, [flockSourceRawData, formikFlockSource]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.setFieldValue('totalQuantity', totalTransferedChicken);
|
formik.setFieldValue('totalQuantity', totalTransferedChicken);
|
||||||
@@ -625,6 +627,7 @@ const TransferToLayingFormModal = () => {
|
|||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-3'>
|
<div className='flex flex-row items-center gap-3'>
|
||||||
<input
|
<input
|
||||||
|
id={`flock-source-kandang-${item.project_flock_kandang_id}`}
|
||||||
type='radio'
|
type='radio'
|
||||||
name='flockSourceKandang'
|
name='flockSourceKandang'
|
||||||
value={item.project_flock_kandang_id}
|
value={item.project_flock_kandang_id}
|
||||||
@@ -637,13 +640,14 @@ const TransferToLayingFormModal = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
|
htmlFor={`flock-source-kandang-${item.project_flock_kandang_id}`}
|
||||||
className={cn('text-sm text-base-content/50', {
|
className={cn('text-sm text-base-content/50', {
|
||||||
'cursor-pointer': isAvailable,
|
'cursor-pointer': isAvailable,
|
||||||
'cursor-not-allowed opacity-50': !isAvailable,
|
'cursor-not-allowed opacity-50': !isAvailable,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{item.kandang_name}{' '}
|
{item.kandang_name}{' '}
|
||||||
<span className='text-base-content/20'>{`(Max: ${item.available_qty})`}</span>
|
<span className='text-base-content/20'>{`(Max: ${item.available_qty ?? '-'})`}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -818,11 +822,33 @@ const TransferToLayingFormModal = () => {
|
|||||||
|
|
||||||
{/* Source Kandang */}
|
{/* Source Kandang */}
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<span className='w-full py-2 text-xs font-semibold'>
|
<span className='w-fit py-2 text-xs font-semibold flex flex-row items-center gap-3'>
|
||||||
Kandang Asal{' '}
|
<span className='text-nowrap'>
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
Kandang Asal{' '}
|
||||||
<span className='text-error'> *</span>
|
<span
|
||||||
|
className='tooltip tooltip-error'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'> *</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div className='w-px h-5 bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
totalAvailableChickenForTransfer < 0
|
||||||
|
? 'error'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
text={`Sisa ayam: ${formatNumber(
|
||||||
|
totalAvailableChickenForTransfer,
|
||||||
|
'en-US'
|
||||||
|
)} ekor`}
|
||||||
|
className={{
|
||||||
|
badge: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{formik.values.flockSourceKandangs.length === 0 && (
|
{formik.values.flockSourceKandangs.length === 0 && (
|
||||||
@@ -902,23 +928,6 @@ const TransferToLayingFormModal = () => {
|
|||||||
<span className='text-error'> *</span>
|
<span className='text-error'> *</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='w-px h-5 bg-base-content/10' />
|
|
||||||
|
|
||||||
<StatusBadge
|
|
||||||
color={
|
|
||||||
totalAvailableChickenForTransfer < 0
|
|
||||||
? 'error'
|
|
||||||
: 'neutral'
|
|
||||||
}
|
|
||||||
text={`Sisa transfer: ${formatNumber(
|
|
||||||
totalAvailableChickenForTransfer,
|
|
||||||
'en-US'
|
|
||||||
)} ekor`}
|
|
||||||
className={{
|
|
||||||
badge: 'text-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{formik.values.flockDestinationKandangs.length === 0 && (
|
{formik.values.flockDestinationKandangs.length === 0 && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -26,10 +26,9 @@ import TransferToLayingFilterModal from '@/components/pages/production/transfer-
|
|||||||
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
|
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
|
||||||
import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton';
|
import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton';
|
||||||
|
|
||||||
import {
|
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
||||||
TransferToLaying,
|
import { TransferToLayingFilterValues } from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter';
|
||||||
TransferToLayingFilter,
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
} from '@/types/api/production/transfer-to-laying';
|
|
||||||
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -142,6 +141,8 @@ const TransferToLayingsTable = () => {
|
|||||||
status: '',
|
status: '',
|
||||||
filter_by: '',
|
filter_by: '',
|
||||||
sort_by: '',
|
sort_by: '',
|
||||||
|
flockSourceNames: '',
|
||||||
|
flockDestinationNames: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -154,6 +155,9 @@ const TransferToLayingsTable = () => {
|
|||||||
filter_by: 'filter_by',
|
filter_by: 'filter_by',
|
||||||
sort_by: 'sort_by',
|
sort_by: 'sort_by',
|
||||||
},
|
},
|
||||||
|
excludeKeysFromUrl: ['flockSourceNames', 'flockDestinationNames'],
|
||||||
|
persist: true,
|
||||||
|
storeName: 'transfer-to-laying-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -431,12 +435,84 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterSubmitHandler = (values: TransferToLayingFilter) => {
|
const STATUS_FILTER_OPTIONS = [
|
||||||
updateFilter('startDate', values.startDate);
|
{ value: 'PENDING', label: 'Pengajuan' },
|
||||||
updateFilter('endDate', values.endDate);
|
{ value: 'APPROVED', label: 'Disetujui' },
|
||||||
updateFilter('flockSource', values.flockSource.join(','));
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
updateFilter('flockDestination', values.flockDestination.join(','));
|
];
|
||||||
updateFilter('status', values.status.join(','));
|
|
||||||
|
const filterModalInitialValues = useMemo(() => {
|
||||||
|
const flockSourceIds = tableFilterState.flockSource
|
||||||
|
? tableFilterState.flockSource.split(',')
|
||||||
|
: [];
|
||||||
|
const flockSourceNameList = tableFilterState.flockSourceNames
|
||||||
|
? tableFilterState.flockSourceNames.split(',')
|
||||||
|
: [];
|
||||||
|
const flockSourceOptions = flockSourceIds.filter(Boolean).map((id, i) => ({
|
||||||
|
value: parseInt(id),
|
||||||
|
label: flockSourceNameList[i] || id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const flockDestIds = tableFilterState.flockDestination
|
||||||
|
? tableFilterState.flockDestination.split(',')
|
||||||
|
: [];
|
||||||
|
const flockDestNameList = tableFilterState.flockDestinationNames
|
||||||
|
? tableFilterState.flockDestinationNames.split(',')
|
||||||
|
: [];
|
||||||
|
const flockDestOptions = flockDestIds.filter(Boolean).map((id, i) => ({
|
||||||
|
value: parseInt(id),
|
||||||
|
label: flockDestNameList[i] || id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const statusIds = tableFilterState.status
|
||||||
|
? tableFilterState.status.split(',')
|
||||||
|
: [];
|
||||||
|
const statusOptions = statusIds.filter(Boolean).map((id) => {
|
||||||
|
const found = STATUS_FILTER_OPTIONS.find((opt) => opt.value === id);
|
||||||
|
return found || { value: id, label: id };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate: tableFilterState.startDate || '',
|
||||||
|
endDate: tableFilterState.endDate || '',
|
||||||
|
flockSource: flockSourceOptions,
|
||||||
|
flockDestination: flockDestOptions,
|
||||||
|
status: statusOptions,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
tableFilterState.startDate,
|
||||||
|
tableFilterState.endDate,
|
||||||
|
tableFilterState.flockSource,
|
||||||
|
tableFilterState.flockDestination,
|
||||||
|
tableFilterState.status,
|
||||||
|
tableFilterState.flockSourceNames,
|
||||||
|
tableFilterState.flockDestinationNames,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filterSubmitHandler = (values: TransferToLayingFilterValues) => {
|
||||||
|
const flockSourceOpts = (values.flockSource as OptionType[]) || [];
|
||||||
|
const flockDestOpts = (values.flockDestination as OptionType[]) || [];
|
||||||
|
const statusOpts = (values.status as OptionType[]) || [];
|
||||||
|
|
||||||
|
updateFilter('startDate', values.startDate || '');
|
||||||
|
updateFilter('endDate', values.endDate || '');
|
||||||
|
updateFilter(
|
||||||
|
'flockSource',
|
||||||
|
flockSourceOpts.map((o) => String(o.value)).join(',')
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'flockDestination',
|
||||||
|
flockDestOpts.map((o) => String(o.value)).join(',')
|
||||||
|
);
|
||||||
|
updateFilter('status', statusOpts.map((o) => String(o.value)).join(','));
|
||||||
|
updateFilter(
|
||||||
|
'flockSourceNames',
|
||||||
|
flockSourceOpts.map((o) => String(o.label)).join(',')
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'flockDestinationNames',
|
||||||
|
flockDestOpts.map((o) => String(o.label)).join(',')
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
@@ -445,6 +521,8 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('flockSource', '');
|
updateFilter('flockSource', '');
|
||||||
updateFilter('flockDestination', '');
|
updateFilter('flockDestination', '');
|
||||||
updateFilter('status', '');
|
updateFilter('status', '');
|
||||||
|
updateFilter('flockSourceNames', '');
|
||||||
|
updateFilter('flockDestinationNames', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportToExcelHandler = async () => {
|
const exportToExcelHandler = async () => {
|
||||||
@@ -558,6 +636,8 @@ const TransferToLayingsTable = () => {
|
|||||||
'search',
|
'search',
|
||||||
'filter_by',
|
'filter_by',
|
||||||
'sort_by',
|
'sort_by',
|
||||||
|
'flockSourceNames',
|
||||||
|
'flockDestinationNames',
|
||||||
]}
|
]}
|
||||||
fieldGroups={[['startDate', 'endDate']]}
|
fieldGroups={[['startDate', 'endDate']]}
|
||||||
onClick={filterModal.openModal}
|
onClick={filterModal.openModal}
|
||||||
@@ -670,6 +750,7 @@ const TransferToLayingsTable = () => {
|
|||||||
|
|
||||||
<TransferToLayingFilterModal
|
<TransferToLayingFilterModal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
|
initialValues={filterModalInitialValues}
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Uniformity } from '@/types/api/production/uniformity';
|
|||||||
|
|
||||||
type UniformityFormSchemaType = {
|
type UniformityFormSchemaType = {
|
||||||
date: string;
|
date: string;
|
||||||
week: number;
|
|
||||||
location?: {
|
location?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -45,10 +44,6 @@ const FileSchema = Yup.mixed<File>()
|
|||||||
export const UniformityFormSchema: Yup.ObjectSchema<UniformityFormSchemaType> =
|
export const UniformityFormSchema: Yup.ObjectSchema<UniformityFormSchemaType> =
|
||||||
Yup.object({
|
Yup.object({
|
||||||
date: Yup.string().required('Tanggal wajib diisi!'),
|
date: Yup.string().required('Tanggal wajib diisi!'),
|
||||||
week: Yup.number()
|
|
||||||
.min(1, 'Minggu ke wajib diisi!')
|
|
||||||
.required('Minggu ke wajib diisi!')
|
|
||||||
.typeError('Minggu ke wajib diisi!'),
|
|
||||||
location: Yup.object({
|
location: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
@@ -81,7 +76,6 @@ export type UniformityFormValues = Yup.InferType<typeof UniformityFormSchema>;
|
|||||||
|
|
||||||
export type UniformityFormData = {
|
export type UniformityFormData = {
|
||||||
date: string;
|
date: string;
|
||||||
week: number;
|
|
||||||
project_flock_kandang_id: number;
|
project_flock_kandang_id: number;
|
||||||
document: File | null;
|
document: File | null;
|
||||||
document_name: string;
|
document_name: string;
|
||||||
@@ -91,8 +85,7 @@ export const getUniformityFormInitialValues = (
|
|||||||
initialValues?: Partial<Uniformity>
|
initialValues?: Partial<Uniformity>
|
||||||
): UniformityFormValues => {
|
): UniformityFormValues => {
|
||||||
return {
|
return {
|
||||||
date: initialValues?.week ? '' : '',
|
date: '',
|
||||||
week: initialValues?.week ?? 0,
|
|
||||||
location: null,
|
location: null,
|
||||||
location_id: 0,
|
location_id: 0,
|
||||||
project_flock: null,
|
project_flock: null,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { LocationApi } from '@/services/api/master-data';
|
|||||||
import {
|
import {
|
||||||
ProjectFlockApi,
|
ProjectFlockApi,
|
||||||
ProjectFlockKandangApi,
|
ProjectFlockKandangApi,
|
||||||
RecordingApi,
|
|
||||||
} from '@/services/api/production';
|
} from '@/services/api/production';
|
||||||
import { UniformityApi } from '@/services/api/uniformity';
|
import { UniformityApi } from '@/services/api/uniformity';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -40,7 +39,6 @@ import {
|
|||||||
ProjectFlockKandangLookup,
|
ProjectFlockKandangLookup,
|
||||||
ProjectFlock,
|
ProjectFlock,
|
||||||
} from '@/types/api/production/project-flock';
|
} from '@/types/api/production/project-flock';
|
||||||
import { Recording } from '@/types/api/production/recording';
|
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
||||||
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
||||||
@@ -204,23 +202,6 @@ const UniformityForm = ({
|
|||||||
? projectFlockKandangLookupData.data
|
? projectFlockKandangLookupData.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// ===== RECORDINGS DATA (FOR WEEK CALCULATION) =====
|
|
||||||
const recordingsUrl = useMemo(() => {
|
|
||||||
if (!projectFlockKandangLookup?.project_flock_kandang_id) return null;
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: '1',
|
|
||||||
limit: '100',
|
|
||||||
project_flock_kandang_id:
|
|
||||||
projectFlockKandangLookup.project_flock_kandang_id.toString(),
|
|
||||||
});
|
|
||||||
return `${RecordingApi.basePath}?${params.toString()}`;
|
|
||||||
}, [projectFlockKandangLookup?.project_flock_kandang_id]);
|
|
||||||
|
|
||||||
const { data: recordingsData } = useSWR(
|
|
||||||
recordingsUrl,
|
|
||||||
recordingsUrl ? RecordingApi.getAllFetcher : null
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FORM CONFIGURATION =====
|
// ===== FORM CONFIGURATION =====
|
||||||
const formikInitialValues = useMemo<UniformityFormValues>(
|
const formikInitialValues = useMemo<UniformityFormValues>(
|
||||||
() => getUniformityFormInitialValues(initialValues),
|
() => getUniformityFormInitialValues(initialValues),
|
||||||
@@ -246,7 +227,6 @@ const UniformityForm = ({
|
|||||||
|
|
||||||
setUniformityFormData({
|
setUniformityFormData({
|
||||||
date: values.date,
|
date: values.date,
|
||||||
week: values.week,
|
|
||||||
project_flock_kandang_id: projectFlockKandangId,
|
project_flock_kandang_id: projectFlockKandangId,
|
||||||
document: values.document as File,
|
document: values.document as File,
|
||||||
document_name: (values.document as File).name,
|
document_name: (values.document as File).name,
|
||||||
@@ -475,59 +455,6 @@ const UniformityForm = ({
|
|||||||
generateUniformityTemplate(population, projectFlockKandangLookup);
|
generateUniformityTemplate(population, projectFlockKandangLookup);
|
||||||
}, [projectFlockKandangLookup]);
|
}, [projectFlockKandangLookup]);
|
||||||
|
|
||||||
// ===== SIDE EFFECTS =====
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
projectFlockKandangLookup?.chick_in_date &&
|
|
||||||
projectFlockKandangLookup?.project_flock_kandang_id
|
|
||||||
) {
|
|
||||||
const chickInDate = new Date(projectFlockKandangLookup.chick_in_date);
|
|
||||||
chickInDate.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
let initialWeek = 18;
|
|
||||||
|
|
||||||
if (
|
|
||||||
isResponseSuccess(recordingsData) &&
|
|
||||||
recordingsData.data &&
|
|
||||||
recordingsData.data.length > 0
|
|
||||||
) {
|
|
||||||
const sortedRecordings = [...recordingsData.data].sort(
|
|
||||||
(a: Recording, b: Recording) =>
|
|
||||||
new Date(a.record_datetime).getTime() -
|
|
||||||
new Date(b.record_datetime).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
const earliestRecording = sortedRecordings[0];
|
|
||||||
if (earliestRecording?.project_flock?.production_standart?.week) {
|
|
||||||
initialWeek =
|
|
||||||
earliestRecording.project_flock.production_standart.week;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formik.values.date) {
|
|
||||||
const selectedDate = new Date(formik.values.date);
|
|
||||||
selectedDate.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
const daysDiff = Math.floor(
|
|
||||||
(selectedDate.getTime() - chickInDate.getTime()) /
|
|
||||||
(1000 * 60 * 60 * 24)
|
|
||||||
);
|
|
||||||
|
|
||||||
const weeksDiff = Math.floor(daysDiff / 7);
|
|
||||||
|
|
||||||
setFieldValue('week', initialWeek + weeksDiff);
|
|
||||||
} else {
|
|
||||||
setFieldValue('week', initialWeek);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
projectFlockKandangLookup?.chick_in_date,
|
|
||||||
projectFlockKandangLookup?.project_flock_kandang_id,
|
|
||||||
recordingsData,
|
|
||||||
formik.values.date,
|
|
||||||
setFieldValue,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsub = subscribeValidate(() => {
|
const unsub = subscribeValidate(() => {
|
||||||
setIsValid(true);
|
setIsValid(true);
|
||||||
@@ -597,6 +524,7 @@ const UniformityForm = ({
|
|||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={formik.touched.date && Boolean(formik.errors.date)}
|
isError={formik.touched.date && Boolean(formik.errors.date)}
|
||||||
errorMessage={formik.errors.date as string}
|
errorMessage={formik.errors.date as string}
|
||||||
|
disabled={isNextStep}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -615,6 +543,7 @@ const UniformityForm = ({
|
|||||||
errorMessage={formik.errors.location_id as string}
|
errorMessage={formik.errors.location_id as string}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isDisabled={isNextStep}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -627,7 +556,7 @@ const UniformityForm = ({
|
|||||||
onInputChange={setProjectFlockSearchValue}
|
onInputChange={setProjectFlockSearchValue}
|
||||||
isLoading={isLoadingProjectFlocks}
|
isLoading={isLoadingProjectFlocks}
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isDisabled={!formik.values.location_id}
|
isDisabled={!formik.values.location_id || isNextStep}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.project_flock_id &&
|
formik.touched.project_flock_id &&
|
||||||
Boolean(formik.errors.project_flock_id)
|
Boolean(formik.errors.project_flock_id)
|
||||||
@@ -644,7 +573,7 @@ const UniformityForm = ({
|
|||||||
value={formik.values.kandang}
|
value={formik.values.kandang}
|
||||||
onChange={handleKandangChange}
|
onChange={handleKandangChange}
|
||||||
options={kandangOptions}
|
options={kandangOptions}
|
||||||
isDisabled={!formik.values.project_flock_id}
|
isDisabled={!formik.values.project_flock_id || isNextStep}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const UniformityResultForm = () => {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
date: uniformityFormData.date,
|
date: uniformityFormData.date,
|
||||||
week: uniformityFormData.week,
|
|
||||||
project_flock_kandang_id: uniformityFormData.project_flock_kandang_id,
|
project_flock_kandang_id: uniformityFormData.project_flock_kandang_id,
|
||||||
document: uniformityFormData.document,
|
document: uniformityFormData.document,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { RefObject, useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
|
import SelectInput from '@/components/input/SelectInput';
|
||||||
|
|
||||||
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { PurchaseFilter } from '@/types/api/purchase/purchase';
|
||||||
|
import { AreaApi, LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
|
import { Area } from '@/types/api/master-data/area';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
interface PurchaseFilterModalProps {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
initialValues?: {
|
||||||
|
poDate: string;
|
||||||
|
category: OptionType<number>[];
|
||||||
|
status: OptionType<string>[];
|
||||||
|
supplier: OptionType<number> | null;
|
||||||
|
area: OptionType<number> | null;
|
||||||
|
location: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
|
};
|
||||||
|
onSubmit?: (values: PurchaseFilter) => void;
|
||||||
|
onReset?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PurchaseFilterModal = ({
|
||||||
|
ref,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: PurchaseFilterModalProps) => {
|
||||||
|
const closeModalHandler = useCallback(() => {
|
||||||
|
ref.current?.close();
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
// ===== DATE ERROR STATE =====
|
||||||
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
|
|
||||||
|
// ===== CLEANUP TOAST ON UNMOUNT =====
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [dateErrorShown]);
|
||||||
|
|
||||||
|
// ===== CLEANUP TOAST WHEN MODAL CLOSES =====
|
||||||
|
useEffect(() => {
|
||||||
|
const dialogElement = ref.current;
|
||||||
|
const handleModalClose = () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialogElement?.addEventListener('close', handleModalClose);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dialogElement?.removeEventListener('close', handleModalClose);
|
||||||
|
};
|
||||||
|
}, [ref, dateErrorShown]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProductCategoryInputValue,
|
||||||
|
options: productCategoryOptions,
|
||||||
|
isLoadingOptions: isLoadingProductCategoryOptions,
|
||||||
|
loadMore: loadMoreProductCategory,
|
||||||
|
} = useSelect<ProductCategory>(
|
||||||
|
ProductCategoryApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedAreaId, setSelectedAreaId] = useState(
|
||||||
|
initialValues?.area?.value ? String(initialValues.area.value) : ''
|
||||||
|
);
|
||||||
|
const [selectedLocationId, setSelectedLocationId] = useState(
|
||||||
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setSupplierInputValue,
|
||||||
|
options: supplierOptions,
|
||||||
|
isLoadingOptions: isLoadingSupplierOptions,
|
||||||
|
loadMore: loadMoreSuppliers,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setAreaInputValue,
|
||||||
|
options: areaOptions,
|
||||||
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
|
} = useSelect<Area>(AreaApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
|
area_id: selectedAreaId || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProjectFlockInputValue,
|
||||||
|
options: projectFlockOptions,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
location_id: selectedLocationId || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik<{
|
||||||
|
poDate: string;
|
||||||
|
category: { label: string; value: number }[];
|
||||||
|
status: { label: string; value: string }[];
|
||||||
|
supplier: OptionType<number> | null;
|
||||||
|
area: OptionType<number> | null;
|
||||||
|
location: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
|
}>({
|
||||||
|
// enableReinitialize: true,
|
||||||
|
initialValues: initialValues || {
|
||||||
|
poDate: '',
|
||||||
|
category: [],
|
||||||
|
status: [],
|
||||||
|
supplier: null,
|
||||||
|
area: null,
|
||||||
|
location: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
|
},
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const formattedValues = {
|
||||||
|
...values,
|
||||||
|
category: values.category.map((item) => String(item.value)),
|
||||||
|
category_labels: values.category,
|
||||||
|
status: values.status.map((item) => String(item.value)),
|
||||||
|
supplier_id: values.supplier?.value,
|
||||||
|
supplier_label: values.supplier?.label,
|
||||||
|
area_id: values.area?.value,
|
||||||
|
area_label: values.area?.label,
|
||||||
|
location_id: values.location?.value,
|
||||||
|
location_label: values.location?.label,
|
||||||
|
project_flock_id: values.project_flock?.value,
|
||||||
|
project_flock_label: values.project_flock?.label,
|
||||||
|
project_flock_kandang_id: values.project_flock_kandang?.value,
|
||||||
|
project_flock_kandang_label: values.project_flock_kandang?.label,
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit?.(formattedValues);
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
setSelectedAreaId('');
|
||||||
|
setSelectedLocationId('');
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { resetForm, submitForm } = formik;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedAreaId(
|
||||||
|
initialValues?.area?.value ? String(initialValues.area.value) : ''
|
||||||
|
);
|
||||||
|
setSelectedLocationId(
|
||||||
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
|
);
|
||||||
|
}, [initialValues?.area, initialValues?.location]);
|
||||||
|
|
||||||
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!formik.values.project_flock ||
|
||||||
|
!projectFlocksRawData ||
|
||||||
|
!isResponseSuccess(projectFlocksRawData)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProjectFlock = projectFlocksRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.project_flock?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedProjectFlock?.kandangs?.map((item) => ({
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock, projectFlocksRawData]);
|
||||||
|
|
||||||
|
const productCategoryChangeHandler = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
formik.setFieldValue('category', val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldValue('status', val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formikResetHandler = useCallback(() => {
|
||||||
|
resetForm({
|
||||||
|
values: {
|
||||||
|
poDate: '',
|
||||||
|
category: [],
|
||||||
|
status: [],
|
||||||
|
supplier: null,
|
||||||
|
area: null,
|
||||||
|
location: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSelectedAreaId('');
|
||||||
|
setSelectedLocationId('');
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset, closeModalHandler]);
|
||||||
|
|
||||||
|
const formikSubmitHandler = useCallback(async () => {
|
||||||
|
await submitForm();
|
||||||
|
}, [submitForm]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'p-0 rounded-xl',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formikResetHandler}
|
||||||
|
className='w-full flex flex-col'
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='text-sm font-medium'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
closeModalHandler();
|
||||||
|
}}
|
||||||
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<DateInput
|
||||||
|
label='PO Date'
|
||||||
|
name='poDate'
|
||||||
|
placeholder='Pilih Tanggal'
|
||||||
|
value={formik.values.poDate}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
isNestedModal
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputCheckbox
|
||||||
|
label='Kategori'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
value={formik.values.category}
|
||||||
|
onChange={productCategoryChangeHandler}
|
||||||
|
options={productCategoryOptions}
|
||||||
|
isLoading={isLoadingProductCategoryOptions}
|
||||||
|
onInputChange={setProductCategoryInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProductCategory}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputCheckbox
|
||||||
|
label='Status'
|
||||||
|
placeholder='Status'
|
||||||
|
value={formik.values.status}
|
||||||
|
onChange={statusChangeHandler}
|
||||||
|
options={PURCHASE_ORDER_APPROVAL_LINE.map((item) => ({
|
||||||
|
label: item.step_name,
|
||||||
|
value: item.step_name,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Vendor'
|
||||||
|
placeholder='Pilih Vendor'
|
||||||
|
value={formik.values.supplier}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'supplier',
|
||||||
|
!Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options={supplierOptions}
|
||||||
|
isLoading={isLoadingSupplierOptions}
|
||||||
|
onInputChange={setSupplierInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
value={formik.values.area}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = !Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null;
|
||||||
|
formik.setFieldValue('area', nextValue);
|
||||||
|
formik.setFieldValue('location', null);
|
||||||
|
formik.setFieldValue('project_flock', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
setSelectedAreaId(
|
||||||
|
nextValue?.value ? String(nextValue.value) : ''
|
||||||
|
);
|
||||||
|
setSelectedLocationId('');
|
||||||
|
}}
|
||||||
|
options={areaOptions}
|
||||||
|
isLoading={isLoadingAreaOptions}
|
||||||
|
onInputChange={setAreaInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
value={formik.values.location}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = !Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null;
|
||||||
|
formik.setFieldValue('location', nextValue);
|
||||||
|
formik.setFieldValue('project_flock', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
setSelectedLocationId(
|
||||||
|
nextValue?.value ? String(nextValue.value) : ''
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={locationOptions}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.area}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = !Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null;
|
||||||
|
formik.setFieldValue('project_flock', nextValue);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
}}
|
||||||
|
options={projectFlockOptions}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.location}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
value={formik.values.project_flock_kandang}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_kandang',
|
||||||
|
!Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options={projectFlockKandangOptions}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.project_flock}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 rounded-lg text-base-content/65'
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
onClick={formikSubmitHandler}
|
||||||
|
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PurchaseFilterModal;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user