mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-26 08:15:44 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4376eeea85 | |||
| f83474ffbf | |||
| 981755ff68 | |||
| dbbc421ce6 | |||
| cadd5b09ba | |||
| 0ced3f3bac | |||
| 5b368ffb78 | |||
| d1b918bcbf | |||
| e6c4a731b1 | |||
| 3dd3172738 | |||
| fa09e140c0 | |||
| 3951f197e3 |
+4
-28
@@ -73,8 +73,8 @@ stages:
|
||||
|
||||
if [ "$CI_COMMIT_BRANCH" = "development" ]; then
|
||||
ENVIRONMENT_NAME="WEB-LTI-DEV"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "staging" ]; then
|
||||
ENVIRONMENT_NAME="WEB-LTI-STAGING"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "master" ]; then
|
||||
ENVIRONMENT_NAME="WEB-LTI-PROD"
|
||||
else
|
||||
ENVIRONMENT_NAME="UNKNOWN"
|
||||
fi
|
||||
@@ -122,6 +122,8 @@ build:dev:
|
||||
environment:
|
||||
name: development
|
||||
variables:
|
||||
# NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
|
||||
# NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
|
||||
NEXT_PUBLIC_LTI_URL: 'https://dev-lti-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'
|
||||
@@ -139,32 +141,6 @@ deploy:dev:
|
||||
environment:
|
||||
name: development
|
||||
url: https://dev-lti-erp.mbugroup.id
|
||||
|
||||
# ====== STAGING (Branch staging) ======
|
||||
build:staging:
|
||||
<<: *build_template
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||
environment:
|
||||
name: staging
|
||||
variables:
|
||||
NEXT_PUBLIC_LTI_URL: 'https://stg-lti-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_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||
|
||||
deploy:staging:
|
||||
<<: *deploy_template
|
||||
needs: ['build:staging']
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||
variables:
|
||||
S3_BUCKET: 'stg-lti-erp.mbugroup.id'
|
||||
AWS_REGION: 'ap-southeast-3'
|
||||
CLOUDFRONT_DISTRIBUTION_ID: 'E2V6PPO1AUIU7H'
|
||||
environment:
|
||||
name: staging
|
||||
url: https://stg-lti-erp.mbugroup.id
|
||||
# ====== PRODUCTION ======
|
||||
# build:production:
|
||||
# <<: *build_template
|
||||
|
||||
Generated
+8
-249
@@ -14,10 +14,8 @@
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"formik": "^2.4.6",
|
||||
"jspdf": "^3.0.4",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"next": "15.5.9",
|
||||
"next": "^15.5.7",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "19.1.0",
|
||||
@@ -28,7 +26,6 @@
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"use-debounce": "^10.0.6",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"yup": "^1.7.0",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
@@ -1085,9 +1082,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
|
||||
"integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz",
|
||||
"integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -1847,25 +1844,12 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
@@ -1895,13 +1879,6 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
|
||||
@@ -2801,16 +2778,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -2960,26 +2927,6 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -3075,18 +3022,6 @@
|
||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.47.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
|
||||
"integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
@@ -3124,16 +3059,6 @@
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -3354,16 +3279,6 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -4085,23 +4000,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-png/node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
@@ -4112,12 +4010,6 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -4605,20 +4497,6 @@
|
||||
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "9.1.7",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||
@@ -4698,12 +4576,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@@ -5239,33 +5111,6 @@
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
|
||||
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf-autotable": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
|
||||
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"jspdf": "^2 || ^3"
|
||||
}
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
@@ -5815,12 +5660,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.5.9",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
|
||||
"integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
|
||||
"version": "15.5.7",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz",
|
||||
"integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.5.9",
|
||||
"@next/env": "15.5.7",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
@@ -6170,13 +6015,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -6330,16 +6168,6 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
@@ -6500,13 +6328,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||
@@ -6599,16 +6420,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -6958,16 +6769,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||
@@ -7187,16 +6988,6 @@
|
||||
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
|
||||
@@ -7241,16 +7032,6 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
@@ -7625,16 +7406,6 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-compatible-readable-stream": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
|
||||
@@ -7764,18 +7535,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.20.3",
|
||||
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
|
||||
+1
-4
@@ -17,10 +17,8 @@
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"formik": "^2.4.6",
|
||||
"jspdf": "^3.0.4",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"next": "15.5.9",
|
||||
"next": "^15.5.7",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "19.1.0",
|
||||
@@ -31,7 +29,6 @@
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"use-debounce": "^10.0.6",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"yup": "^1.7.0",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
|
||||
@@ -24,11 +24,6 @@ const ClosingDetailPage = () => {
|
||||
() => ClosingApi.getPenjualan(Number(closingId))
|
||||
);
|
||||
|
||||
const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR(
|
||||
closingId ? `hpp-ekspedisi-${closingId}` : null,
|
||||
() => ClosingApi.getHppEkspedisi(Number(closingId))
|
||||
);
|
||||
|
||||
if (!closingId) {
|
||||
router.back();
|
||||
|
||||
@@ -44,7 +39,7 @@ const ClosingDetailPage = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const isLoading = isLoadingClosing || isLoadingSales || isLoadingHppEkspedisi;
|
||||
const isLoading = isLoadingClosing || isLoadingSales;
|
||||
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
@@ -55,11 +50,6 @@ const ClosingDetailPage = () => {
|
||||
id={Number(closingId)}
|
||||
initialValue={closing.data}
|
||||
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
|
||||
hppExpeditionData={
|
||||
isResponseSuccess(hppEkspedisiData)
|
||||
? hppEkspedisiData.data
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import FinanceForm from '@/components/pages/finance/form/FinanceForm';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Finance | Add',
|
||||
};
|
||||
|
||||
const FinanceAddPage = () => {
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
<FinanceForm formType='add' />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinanceAddPage;
|
||||
@@ -0,0 +1,11 @@
|
||||
import FinancesTable from '@/components/pages/finance/FinancesTable';
|
||||
|
||||
const Finance = () => {
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<FinancesTable />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Finance;
|
||||
@@ -14,13 +14,13 @@ const ProjectFlockClosingPage = () => {
|
||||
const projectFlockKandangId = searchParams.get('projectFlockKandangId');
|
||||
|
||||
const { data: projectFlockKandang, isLoading: isLoadingProjectFlockKandang } =
|
||||
useSWR(`get-flock-kandang-id/${projectFlockKandangId}`, () =>
|
||||
ProjectFlockKandangApi.getSingle(parseInt(projectFlockKandangId ?? ''))
|
||||
useSWR(projectFlockKandangId, (id: number) =>
|
||||
ProjectFlockKandangApi.getSingle(id)
|
||||
);
|
||||
|
||||
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
||||
`get-flock-id/${projectFlockId}`,
|
||||
() => ProjectFlockApi.getSingle(parseInt(projectFlockId ?? ''))
|
||||
projectFlockId,
|
||||
(id: number) => ProjectFlockApi.getSingle(id)
|
||||
);
|
||||
|
||||
if (!projectFlockId || !projectFlockKandangId) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function ProjectFlockLayout({
|
||||
const router = useRouter();
|
||||
const toggleValidate = useUiStore((s) => s.toggleValidate);
|
||||
|
||||
const isAdd = pathname.includes('/add');
|
||||
const isAdd = pathname.endsWith('/add');
|
||||
const isEdit = pathname.includes('/detail/edit');
|
||||
const isDetail = pathname.includes('/detail');
|
||||
const isChickin = pathname.includes('/chickin/add/kandang');
|
||||
|
||||
@@ -14,7 +14,7 @@ const RecordingEdit = () => {
|
||||
|
||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||
recordingId,
|
||||
(id: string) => RecordingApi.getSingle(parseInt(id))
|
||||
(id: number) => RecordingApi.getSingle(id) // Gunakan RecordingApi
|
||||
);
|
||||
|
||||
if (!recordingId) {
|
||||
|
||||
@@ -14,7 +14,7 @@ const RecordingDetail = () => {
|
||||
|
||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||
recordingId,
|
||||
(id: string) => RecordingApi.getSingle(parseInt(id))
|
||||
(id: number) => RecordingApi.getSingle(id)
|
||||
);
|
||||
|
||||
if (!recordingId) {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,5 +0,0 @@
|
||||
const ReportExpenseDetail = () => {
|
||||
return <div>ReportExpenseDetail</div>;
|
||||
};
|
||||
|
||||
export default ReportExpenseDetail;
|
||||
@@ -1,13 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import ReportExpenseTable from '@/components/pages/report/expense/ReportExpenseTable';
|
||||
|
||||
const ReportExpense = () => {
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
<ReportExpenseTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportExpense;
|
||||
@@ -1,11 +0,0 @@
|
||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,7 +0,0 @@
|
||||
import LogisticStockTabs from '@/components/pages/report/logistic-stock/LogisticStockTabs';
|
||||
|
||||
const LogisticStock = () => {
|
||||
return <LogisticStockTabs />;
|
||||
};
|
||||
|
||||
export default LogisticStock;
|
||||
@@ -1,11 +0,0 @@
|
||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,11 +0,0 @@
|
||||
import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
|
||||
|
||||
const MarketingReportPage = () => {
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<MarketingReportContent />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingReportPage;
|
||||
@@ -1,114 +0,0 @@
|
||||
import React, { ReactNode, useState, useRef } from 'react';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
export interface DropdownProps {
|
||||
trigger: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: {
|
||||
wrapper?: string;
|
||||
trigger?: string;
|
||||
content?: string;
|
||||
};
|
||||
align?: 'start' | 'center' | 'end';
|
||||
direction?: 'top' | 'bottom' | 'left' | 'right';
|
||||
hover?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
close?: boolean;
|
||||
controlled?: boolean;
|
||||
}
|
||||
|
||||
const Dropdown = ({
|
||||
trigger,
|
||||
children,
|
||||
className,
|
||||
align,
|
||||
direction,
|
||||
hover,
|
||||
defaultOpen = false,
|
||||
open,
|
||||
close,
|
||||
controlled = false,
|
||||
}: DropdownProps) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!controlled) {
|
||||
const newState = !isOpen;
|
||||
setIsOpen(newState);
|
||||
}
|
||||
};
|
||||
|
||||
const getWrapperClasses = () => {
|
||||
const openState = controlled ? open : isOpen;
|
||||
|
||||
return cn(
|
||||
'dropdown',
|
||||
{
|
||||
'dropdown-start': align === 'start',
|
||||
'dropdown-center': align === 'center',
|
||||
'dropdown-end': align === 'end',
|
||||
'dropdown-top': direction === 'top',
|
||||
'dropdown-bottom': direction === 'bottom',
|
||||
'dropdown-left': direction === 'left',
|
||||
'dropdown-right': direction === 'right',
|
||||
'dropdown-hover': hover,
|
||||
'dropdown-open': openState && !close,
|
||||
'dropdown-close': close,
|
||||
},
|
||||
className?.wrapper
|
||||
);
|
||||
};
|
||||
|
||||
const getTriggerClasses = () => {
|
||||
return cn(className?.trigger);
|
||||
};
|
||||
|
||||
const getContentClasses = () => {
|
||||
return cn(
|
||||
'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box',
|
||||
className?.content
|
||||
);
|
||||
};
|
||||
|
||||
if (controlled) {
|
||||
return (
|
||||
<div className={getWrapperClasses()}>
|
||||
{trigger}
|
||||
{open && !close && (
|
||||
<div tabIndex={-1} className={getContentClasses()}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className={getWrapperClasses()}>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
className={getTriggerClasses()}
|
||||
onClick={toggleDropdown}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{trigger}
|
||||
</div>
|
||||
{!close && (
|
||||
<div tabIndex={-1} className={getContentClasses()}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
||||
@@ -33,9 +33,7 @@ const FloatingActionsButton = ({
|
||||
}: FloatingActionsButtonProps) => {
|
||||
// Jika tidak ada baris yang dipilih, jangan tampilkan FAB
|
||||
const positionStyles =
|
||||
selectedRowIds.length > 0
|
||||
? 'bottom-[10%] opacity-100'
|
||||
: 'bottom-[-10%] opacity-0';
|
||||
selectedRowIds.length > 0 ? 'bottom-[10%]' : 'bottom-[-100%]';
|
||||
|
||||
// Helper untuk menentukan gaya warna tombol approval
|
||||
const getApprovalColor = (action: 'APPROVED' | 'REJECTED') => {
|
||||
|
||||
@@ -9,13 +9,10 @@ import Drawer from '@/components/Drawer';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Button from '@/components/Button';
|
||||
import SidebarMenu from '@/components/molecules/SidebarMenu';
|
||||
import PermissionNotFound from '@/components/helper/PermissionNotFound';
|
||||
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
||||
import { isPathActive } from '@/lib/helper';
|
||||
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
|
||||
import { useAuth } from '@/services/hooks/useAuth';
|
||||
|
||||
const MainDrawerContent = () => {
|
||||
const pathname = usePathname();
|
||||
@@ -65,11 +62,6 @@ const MainDrawer = ({
|
||||
}>) => {
|
||||
const { mainDrawerOpen, setMainDrawerOpen } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
const { permissionCheck } = useAuth();
|
||||
|
||||
const isPermitted = ROUTE_PERMISSIONS[pathname]?.some((permission) =>
|
||||
permissionCheck(permission)
|
||||
);
|
||||
|
||||
const getPageTitle = useCallback(() => {
|
||||
let title = '';
|
||||
@@ -109,10 +101,6 @@ const MainDrawer = ({
|
||||
setMainDrawerOpen(!mainDrawerOpen);
|
||||
};
|
||||
|
||||
if (!isPermitted) {
|
||||
return <PermissionNotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={mainDrawerOpen}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Icon } from '@iconify/react';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import Dropdown from '@/components/dropdown/Dropdown';
|
||||
|
||||
import { useAuth } from '@/services/hooks/useAuth';
|
||||
import { AuthApi } from '@/services/api/auth';
|
||||
@@ -54,8 +54,7 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Dropdown
|
||||
align='end'
|
||||
direction='bottom'
|
||||
position='bottom-end'
|
||||
trigger={
|
||||
<div className='btn btn-ghost btn-circle avatar'>
|
||||
<div className='w-10 rounded-full border flex justify-center items-center'>
|
||||
@@ -63,11 +62,9 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
className={{
|
||||
content: 'w-52 mt-3',
|
||||
}}
|
||||
contentClassName='w-52 mt-3'
|
||||
>
|
||||
<Menu>
|
||||
<Menu className='p-2 bg-base-100 shadow rounded-box menu-sm'>
|
||||
<MenuItem title='Logout' onClick={logoutClickHandler} />
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
|
||||
+17
-35
@@ -60,12 +60,6 @@ export interface TableProps<TData extends object> {
|
||||
renderFooter?: boolean;
|
||||
withCheckbox?: boolean;
|
||||
rowOptions?: number[];
|
||||
/**
|
||||
* Custom row renderer. Should return a complete <tr> element or null.
|
||||
* This gives full control over the row structure including colspan.
|
||||
* Return null to render the default row.
|
||||
*/
|
||||
renderCustomRow?: (row: Row<TData>) => ReactNode | null;
|
||||
}
|
||||
|
||||
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
||||
@@ -118,7 +112,6 @@ const Table = <TData extends object>({
|
||||
renderFooter = false,
|
||||
withCheckbox = false,
|
||||
rowOptions = [10, 20, 50, 100],
|
||||
renderCustomRow,
|
||||
}: TableProps<TData>) => {
|
||||
const isServerSideTable =
|
||||
totalItems !== undefined &&
|
||||
@@ -312,35 +305,24 @@ const Table = <TData extends object>({
|
||||
</thead>
|
||||
|
||||
<tbody className={tableClassNames.tableBodyClassName}>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
const customRowContent = renderCustomRow?.(row);
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<tr key={row.id} className={tableClassNames.bodyRowClassName}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={cn(
|
||||
{ 'first:w-9 first:pr-0': withCheckbox },
|
||||
tableClassNames.bodyColumnClassName
|
||||
)}
|
||||
>
|
||||
{!isLoading &&
|
||||
flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
|
||||
if (customRowContent) {
|
||||
return renderCustomRow?.(row);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={row.id} className={tableClassNames.bodyRowClassName}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className={cn(
|
||||
{ 'first:w-9 first:pr-0': withCheckbox },
|
||||
tableClassNames.bodyColumnClassName
|
||||
)}
|
||||
>
|
||||
{!isLoading &&
|
||||
flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
|
||||
{isLoading && <div className='skeleton w-full h-4' />}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{isLoading && <div className='skeleton w-full h-4' />}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot className={cn(tableClassNames.tableFooterClassName)}>
|
||||
{renderFooter && (
|
||||
|
||||
+6
-13
@@ -21,7 +21,6 @@ export interface TabsProps
|
||||
className?:
|
||||
| string
|
||||
| {
|
||||
container?: string;
|
||||
wrapper?: string;
|
||||
tab?: string;
|
||||
content?: string;
|
||||
@@ -54,14 +53,10 @@ const Tabs = ({
|
||||
onTabChange?.(tabId);
|
||||
};
|
||||
|
||||
const {
|
||||
container: containerClassName,
|
||||
wrapper: wrapperClassName,
|
||||
tab: tabClassName,
|
||||
content: contentClassName,
|
||||
} = typeof className === 'object'
|
||||
? className
|
||||
: { wrapper: className, tab: undefined };
|
||||
const { wrapper: wrapperClassName, tab: tabClassName } =
|
||||
typeof className === 'object'
|
||||
? className
|
||||
: { wrapper: className, tab: undefined };
|
||||
|
||||
const getTabsClasses = () => {
|
||||
const variantClasses: Record<string, string> = {
|
||||
@@ -109,7 +104,7 @@ const Tabs = ({
|
||||
{...props}
|
||||
className={cn(
|
||||
'w-full',
|
||||
typeof className === 'string' ? className : containerClassName
|
||||
typeof className === 'string' ? className : undefined
|
||||
)}
|
||||
>
|
||||
<div role='tablist' className={getTabsClasses()}>
|
||||
@@ -126,9 +121,7 @@ const Tabs = ({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeContent && (
|
||||
<div className={cn('mt-4', contentClassName)}>{activeContent}</div>
|
||||
)}
|
||||
{activeContent && <div className='mt-4'>{activeContent}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,109 +1,111 @@
|
||||
import React, { ReactNode, useState, useRef } from 'react';
|
||||
'use client';
|
||||
|
||||
import { ReactNode, useRef, useEffect, useState } from 'react';
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
export interface DropdownProps {
|
||||
interface DropdownProps {
|
||||
trigger: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: {
|
||||
wrapper?: string;
|
||||
trigger?: string;
|
||||
content?: string;
|
||||
};
|
||||
position?:
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'left-start'
|
||||
| 'left-end'
|
||||
| 'right-start'
|
||||
| 'right-end';
|
||||
align?: 'start' | 'center' | 'end';
|
||||
direction?: 'top' | 'bottom' | 'left' | 'right';
|
||||
hover?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
close?: boolean;
|
||||
controlled?: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
const Dropdown = ({
|
||||
trigger,
|
||||
children,
|
||||
position = 'bottom',
|
||||
align = 'start',
|
||||
hover = false,
|
||||
className,
|
||||
align,
|
||||
direction,
|
||||
hover,
|
||||
defaultOpen = false,
|
||||
open,
|
||||
close,
|
||||
controlled = false,
|
||||
contentClassName,
|
||||
}: DropdownProps) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!controlled) {
|
||||
const newState = !isOpen;
|
||||
setIsOpen(newState);
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// Build position classes
|
||||
const getPositionClasses = () => {
|
||||
const classes: string[] = [];
|
||||
|
||||
// Handle combined positions like 'top-start'
|
||||
if (position.includes('-')) {
|
||||
const [pos, al] = position.split('-');
|
||||
classes.push(`dropdown-${pos}`);
|
||||
classes.push(`dropdown-${al}`);
|
||||
} else {
|
||||
classes.push(`dropdown-${position}`);
|
||||
if (align !== 'start') {
|
||||
classes.push(`dropdown-${align}`);
|
||||
}
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
};
|
||||
|
||||
const getWrapperClasses = () => {
|
||||
const openState = controlled ? open : isOpen;
|
||||
|
||||
return cn(
|
||||
'dropdown',
|
||||
{
|
||||
'dropdown-start': align === 'start',
|
||||
'dropdown-center': align === 'center',
|
||||
'dropdown-end': align === 'end',
|
||||
'dropdown-top': direction === 'top',
|
||||
'dropdown-bottom': direction === 'bottom',
|
||||
'dropdown-left': direction === 'left',
|
||||
'dropdown-right': direction === 'right',
|
||||
'dropdown-hover': hover,
|
||||
'dropdown-open': openState && !close,
|
||||
'dropdown-close': close,
|
||||
},
|
||||
className?.wrapper
|
||||
);
|
||||
const handleToggle = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// alert('clicked');
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const getTriggerClasses = () => {
|
||||
return cn(className?.trigger);
|
||||
};
|
||||
|
||||
const getContentClasses = () => {
|
||||
return cn(
|
||||
'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box',
|
||||
className?.content
|
||||
);
|
||||
};
|
||||
|
||||
if (controlled) {
|
||||
return (
|
||||
<div className={getWrapperClasses()}>
|
||||
{trigger}
|
||||
{open && !close && (
|
||||
<div tabIndex={-1} className={getContentClasses()}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className={getWrapperClasses()}>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
className={getTriggerClasses()}
|
||||
onClick={toggleDropdown}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className={cn(
|
||||
'dropdown',
|
||||
getPositionClasses(),
|
||||
hover && 'dropdown-hover',
|
||||
isOpen && 'dropdown-open',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Trigger Button */}
|
||||
<div onClick={handleToggle} className='cursor-pointer'>
|
||||
{trigger}
|
||||
</div>
|
||||
{!close && (
|
||||
<div tabIndex={-1} className={getContentClasses()}>
|
||||
|
||||
{/* Dropdown Content - Only render when open */}
|
||||
{isOpen && (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className={cn('dropdown-content z-[10]', contentClassName)}
|
||||
onClick={() => setIsOpen(false)} // Close on item click
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
const PermissionNotFound = () => {
|
||||
return (
|
||||
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
|
||||
<h2 className='text-2xl font-bold text-error'>Permission Not Found</h2>
|
||||
<p className='text-gray-600 text-center'>
|
||||
You do not have permission to access this page.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionNotFound;
|
||||
@@ -27,9 +27,6 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
|
||||
SWRHttpKey
|
||||
>('/sso/userinfo', httpClientFetcher, {
|
||||
shouldRetryOnError: false,
|
||||
|
||||
// refresh every 13 minutes
|
||||
refreshInterval: 13 * 60 * 1000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useAuth } from '@/services/hooks/useAuth';
|
||||
|
||||
interface RequirePermissionProps {
|
||||
children: React.ReactNode;
|
||||
permissions: string | string[];
|
||||
}
|
||||
|
||||
const RequirePermission = ({
|
||||
children,
|
||||
permissions,
|
||||
}: RequirePermissionProps) => {
|
||||
const { permissionCheck } = useAuth();
|
||||
|
||||
const isPermitted =
|
||||
typeof permissions === 'string'
|
||||
? permissionCheck(permissions)
|
||||
: permissions.some((permission) => permissionCheck(permission));
|
||||
|
||||
if (!isPermitted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default RequirePermission;
|
||||
@@ -24,11 +24,6 @@ const DebouncedTextInput = (props: DebouncedTextInputProps) => {
|
||||
setInternalChangeEvent(e);
|
||||
};
|
||||
|
||||
// Sync internal value with external value prop changes (e.g., from reset)
|
||||
useEffect(() => {
|
||||
setInternalValue(props.value);
|
||||
}, [props.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedChangeEvent) {
|
||||
onChange?.(debouncedChangeEvent);
|
||||
|
||||
@@ -8,7 +8,6 @@ interface MenuItemProps {
|
||||
href?: string;
|
||||
icon?: string;
|
||||
active?: boolean;
|
||||
isLoading?: boolean;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
@@ -18,7 +17,6 @@ const MenuItem = ({
|
||||
href,
|
||||
icon,
|
||||
active = false,
|
||||
isLoading = false,
|
||||
className,
|
||||
onClick,
|
||||
}: MenuItemProps) => {
|
||||
@@ -52,28 +50,17 @@ const MenuItem = ({
|
||||
|
||||
return (
|
||||
<li>
|
||||
{!isLoading && href && (
|
||||
{href && (
|
||||
<Link href={href} className={menuItemBaseClassName}>
|
||||
{menuItemContent}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{!isLoading && !href && (
|
||||
{!href && (
|
||||
<button className={menuItemBaseClassName} onClick={onClick}>
|
||||
{menuItemContent}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<button className={menuItemBaseClassName}>
|
||||
<span
|
||||
className={cn('loading loading-dots loading-md mx-auto', {
|
||||
'text-gray-400': !active,
|
||||
'text-black': active,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import Link from 'next/link';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { cn, isPathActive } from '@/lib/helper';
|
||||
import { useAuth } from '@/services/hooks/useAuth';
|
||||
|
||||
export interface SidebarMenuItem {
|
||||
type?: 'item' | 'title';
|
||||
@@ -10,7 +9,6 @@ export interface SidebarMenuItem {
|
||||
link: string;
|
||||
icon?: string;
|
||||
submenu?: SidebarMenuItem[];
|
||||
permission?: string[];
|
||||
}
|
||||
|
||||
interface SidebarMenuItemProps {
|
||||
@@ -24,17 +22,8 @@ interface SidebarMenuProps {
|
||||
}
|
||||
|
||||
const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
|
||||
const { permissionCheck } = useAuth();
|
||||
const isItemActive = isPathActive(activeLink, item.link);
|
||||
|
||||
const isUserPermitted = item.permission
|
||||
? item.permission?.some((permissionName) => permissionCheck(permissionName))
|
||||
: true;
|
||||
|
||||
if (!isUserPermitted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const menuItemWithoutSubmenu = (
|
||||
<li>
|
||||
<Link
|
||||
@@ -89,15 +78,13 @@ const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
|
||||
const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => {
|
||||
return (
|
||||
<Menu>
|
||||
{menu.map((menuItem, menuIdx) => {
|
||||
return (
|
||||
<SidebarMenuItem
|
||||
key={menuIdx}
|
||||
item={menuItem}
|
||||
activeLink={activeLink}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{menu.map((menuItem, menuIdx) => (
|
||||
<SidebarMenuItem
|
||||
key={menuIdx}
|
||||
item={menuItem}
|
||||
activeLink={activeLink}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,45 +144,33 @@ const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => {
|
||||
|
||||
export const formatGroupedApprovalsToApprovalSteps = (
|
||||
approvalLine: ApprovalLine,
|
||||
groupedApprovals: BaseGroupedApproval[] | undefined,
|
||||
latestApproval: BaseApproval | undefined
|
||||
groupedApprovals: BaseGroupedApproval[],
|
||||
latestApproval: BaseApproval
|
||||
): ApprovalStepsProps['approvals'] => {
|
||||
const formattedApprovalSteps: ApprovalStepsProps['approvals'] =
|
||||
approvalLine.map((approvalLineItem) => {
|
||||
const approvalGroup = groupedApprovals?.find(
|
||||
const approvalGroup = groupedApprovals.find(
|
||||
(approvalGroupItem) =>
|
||||
approvalGroupItem.step_number === approvalLineItem.step_number
|
||||
);
|
||||
|
||||
const currentStepNumber = approvalLineItem.step_number;
|
||||
const lastStepNumber =
|
||||
groupedApprovals?.[groupedApprovals.length - 1]?.step_number;
|
||||
groupedApprovals[groupedApprovals.length - 1]?.step_number;
|
||||
|
||||
const isLatestApprovalRejected = latestApproval?.action === 'REJECTED';
|
||||
const isLatestApprovalRejected = latestApproval.action === 'REJECTED';
|
||||
|
||||
// Only throw error if we have a valid lastStepNumber to compare against
|
||||
if (
|
||||
!approvalGroup &&
|
||||
lastStepNumber !== undefined &&
|
||||
currentStepNumber <= lastStepNumber
|
||||
) {
|
||||
// throw new Error(
|
||||
// `Approval dengan ${approvalLineItem.step_name} tidak ditemukan!`
|
||||
// );
|
||||
if (!approvalGroup && currentStepNumber <= lastStepNumber) {
|
||||
throw new Error(
|
||||
`Approval dengan ${approvalLineItem.step_name} tidak ditemukan!`
|
||||
);
|
||||
}
|
||||
|
||||
if (!approvalGroup) {
|
||||
// Check if this step is waiting (only if we have latestApproval)
|
||||
const isWaiting =
|
||||
latestApproval?.step_number !== undefined &&
|
||||
currentStepNumber === latestApproval.step_number + 1;
|
||||
|
||||
// Check if previous approval was rejected
|
||||
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
|
||||
const isPreviousApprovalRejected =
|
||||
groupedApprovals &&
|
||||
groupedApprovals.length > 0 &&
|
||||
groupedApprovals[groupedApprovals.length - 1]?.approvals?.[0]
|
||||
?.action === 'REJECTED';
|
||||
groupedApprovals[groupedApprovals.length - 1].approvals[0].action ===
|
||||
'REJECTED';
|
||||
|
||||
return {
|
||||
name: approvalLineItem.step_name,
|
||||
@@ -196,11 +184,7 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
||||
|
||||
let approvalStatus: ApprovalStepStatus = 'IDLE';
|
||||
|
||||
// Only compare if latestApproval and its step_number exist
|
||||
if (
|
||||
latestApproval?.step_number !== undefined &&
|
||||
approvalGroup.step_number <= latestApproval.step_number
|
||||
) {
|
||||
if (approvalGroup.step_number <= latestApproval.step_number) {
|
||||
if (approvalGroup.approvals) {
|
||||
switch (approvalGroup?.approvals[0]?.action) {
|
||||
case 'CREATED':
|
||||
@@ -219,7 +203,6 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
latestApproval?.step_number !== undefined &&
|
||||
approvalGroup.step_number === latestApproval.step_number + 1 &&
|
||||
!isLatestApprovalRejected
|
||||
) {
|
||||
@@ -370,33 +353,14 @@ const useApprovalSteps = ({
|
||||
|
||||
// Formatting Akhir
|
||||
const approvals = useMemo(() => {
|
||||
if (isLoading || !approvalLines.length) {
|
||||
if (isLoading || !approvalLines.length || !latestApproval) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try to derive latestApproval from groupedApprovals if not provided
|
||||
let effectiveLatestApproval = latestApproval;
|
||||
|
||||
if (!effectiveLatestApproval && groupedApprovals.length > 0) {
|
||||
// Get all approvals from grouped data
|
||||
const allApprovals = groupedApprovals.flatMap((group) => group.approvals);
|
||||
|
||||
if (allApprovals.length > 0) {
|
||||
// Use the most recent approval (last in array)
|
||||
effectiveLatestApproval = allApprovals[allApprovals.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// If still no latestApproval, return empty
|
||||
if (!effectiveLatestApproval) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return formatGroupedApprovalsToApprovalSteps(
|
||||
approvalLines,
|
||||
groupedApprovals,
|
||||
effectiveLatestApproval
|
||||
latestApproval
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('Gagal memformat approval steps:', error);
|
||||
|
||||
@@ -6,32 +6,24 @@ import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import Tabs from '@/components/Tabs';
|
||||
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
||||
import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent';
|
||||
import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent';
|
||||
|
||||
import {
|
||||
ClosingGeneralInformation,
|
||||
BaseClosingSales,
|
||||
ClosingHppExpedition,
|
||||
} from '@/types/api/closing';
|
||||
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
||||
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
|
||||
import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
|
||||
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
|
||||
import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable';
|
||||
import ClosingSapronakTabContent from './ClosingSapronakTabContent';
|
||||
import SalesReportTable from './sale/SalesReportTable';
|
||||
|
||||
interface ClosingDetailProps {
|
||||
id: number;
|
||||
initialValue?: ClosingGeneralInformation;
|
||||
salesData?: BaseClosingSales;
|
||||
hppExpeditionData?: ClosingHppExpedition;
|
||||
}
|
||||
|
||||
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
id,
|
||||
initialValue,
|
||||
salesData,
|
||||
hppExpeditionData,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<string>('sapronak');
|
||||
|
||||
@@ -45,7 +37,7 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
{
|
||||
id: 'perhitunganSapronak',
|
||||
label: 'Perhitungan Sapronak',
|
||||
content: <ClosingSapronakCalculationTabContent projectFlockId={id} />,
|
||||
content: 'Perhitungan Sapronak',
|
||||
},
|
||||
{
|
||||
id: 'penjualan',
|
||||
@@ -55,22 +47,22 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
{
|
||||
id: 'overhead',
|
||||
label: 'Overhead',
|
||||
content: <ClosingOverheadTabContent projectFlockId={id} />,
|
||||
content: 'Overhead',
|
||||
},
|
||||
{
|
||||
id: 'hppEkspedisi',
|
||||
label: 'HPP Ekspedisi',
|
||||
content: <HppExpeditionReportTable initialValues={hppExpeditionData} />,
|
||||
content: 'HPP Ekspedisi',
|
||||
},
|
||||
{
|
||||
id: 'dataProduksi',
|
||||
label: 'Data Produksi',
|
||||
content: <ClosingProductionDataTabContent projectFlockId={id} />,
|
||||
content: 'Data Produksi',
|
||||
},
|
||||
{
|
||||
id: 'keuangan',
|
||||
label: 'Keuangan',
|
||||
content: <ClosingFinanceTabContent projectFlockId={id} />,
|
||||
content: 'Keuangan',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import ClosingFinanceTable from '@/components/pages/closing/ClosingFinanceTable';
|
||||
|
||||
const ClosingFinanceTabContent = ({
|
||||
projectFlockId,
|
||||
}: {
|
||||
projectFlockId: number;
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<ClosingFinanceTable projectFlockId={projectFlockId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingFinanceTabContent;
|
||||
@@ -1,495 +0,0 @@
|
||||
import Card from '@/components/Card';
|
||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import {
|
||||
DataSummarySubTotal,
|
||||
HppPurchaseData,
|
||||
ProfitLossDataAmount,
|
||||
} from '@/types/api/closing';
|
||||
import useSWR from 'swr';
|
||||
|
||||
type HppTableRow =
|
||||
| (HppPurchaseData & {
|
||||
group_name: string;
|
||||
group_index: number;
|
||||
isGroupHeader?: boolean;
|
||||
})
|
||||
| {
|
||||
group_name: string;
|
||||
group_index: number;
|
||||
isGroupHeader: true;
|
||||
type?: never;
|
||||
budgeting?: never;
|
||||
realization?: never;
|
||||
};
|
||||
|
||||
type ProfitLossTableRow =
|
||||
| (DataSummarySubTotal & {
|
||||
type: string;
|
||||
group_name: string;
|
||||
group_index: number;
|
||||
isGroupHeader?: boolean;
|
||||
})
|
||||
| {
|
||||
group_name: string;
|
||||
group_index: number;
|
||||
isGroupHeader: true;
|
||||
type?: never;
|
||||
rp_per_bird?: never;
|
||||
rp_per_kg?: never;
|
||||
amount?: never;
|
||||
};
|
||||
|
||||
const ClosingFinanceTable = ({
|
||||
projectFlockId,
|
||||
}: {
|
||||
projectFlockId: number;
|
||||
}) => {
|
||||
const { data: finance, isLoading } = useSWR(
|
||||
`/closing/finance/${projectFlockId}`,
|
||||
() => ClosingApi.getFinance(projectFlockId)
|
||||
);
|
||||
|
||||
const hppTableData: HppTableRow[] = isResponseSuccess(finance)
|
||||
? finance.data.hpp_purchases.hpp.flatMap((hpp, groupIndex) => [
|
||||
// Group header row
|
||||
{
|
||||
group_name: hpp.group_name,
|
||||
group_index: groupIndex,
|
||||
isGroupHeader: true as const,
|
||||
},
|
||||
// Data rows
|
||||
...hpp.data.map((item) => ({
|
||||
group_name: hpp.group_name,
|
||||
group_index: groupIndex,
|
||||
type: item.type,
|
||||
budgeting: item.budgeting,
|
||||
realization: item.realization,
|
||||
isGroupHeader: false as const,
|
||||
})),
|
||||
])
|
||||
: [];
|
||||
|
||||
const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance)
|
||||
? [
|
||||
// Pembelian group
|
||||
...finance.data.profit_loss.data.pembelian.map((item) => ({
|
||||
label: 'Pembelian',
|
||||
group_name: 'Pembelian',
|
||||
group_index: 1,
|
||||
type: item.type,
|
||||
rp_per_bird: item.rp_per_bird,
|
||||
rp_per_kg: item.rp_per_kg,
|
||||
amount: item.amount,
|
||||
isGroupHeader: false as const,
|
||||
})),
|
||||
{
|
||||
label: finance.data.profit_loss.data.summary.gross_profit.label,
|
||||
group_name: 'Penjualan',
|
||||
group_index: 0,
|
||||
isGroupHeader: true as const,
|
||||
type: finance.data.profit_loss.data.summary.gross_profit.label,
|
||||
rp_per_bird:
|
||||
finance.data.profit_loss.data.summary.gross_profit.rp_per_bird,
|
||||
rp_per_kg:
|
||||
finance.data.profit_loss.data.summary.gross_profit.rp_per_kg,
|
||||
amount: finance.data.profit_loss.data.summary.gross_profit.amount,
|
||||
},
|
||||
// Penjualan group
|
||||
...finance.data.profit_loss.data.penjualan.map((item) => ({
|
||||
label: 'Penjualan',
|
||||
group_name: 'Penjualan',
|
||||
group_index: 0,
|
||||
type: item.type,
|
||||
rp_per_bird: item.rp_per_bird,
|
||||
rp_per_kg: item.rp_per_kg,
|
||||
amount: item.amount,
|
||||
isGroupHeader: false as const,
|
||||
})),
|
||||
{
|
||||
label: finance.data.profit_loss.data.summary.sub_total.label,
|
||||
group_name: 'Pembelian',
|
||||
group_index: 1,
|
||||
isGroupHeader: true as const,
|
||||
type: finance.data.profit_loss.data.summary.sub_total.label,
|
||||
rp_per_bird:
|
||||
finance.data.profit_loss.data.summary.sub_total.rp_per_bird,
|
||||
rp_per_kg: finance.data.profit_loss.data.summary.sub_total.rp_per_kg,
|
||||
amount: finance.data.profit_loss.data.summary.sub_total.amount,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<>
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-6'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div>
|
||||
{isResponseSuccess(finance)
|
||||
? formatTitleCase(
|
||||
finance.data.profit_loss.data.summary.gross_profit
|
||||
.label || '-'
|
||||
)
|
||||
: 'Laba Rugi Brutto'}
|
||||
</div>
|
||||
<div className='text-lg font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.gross_profit.amount
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div>
|
||||
{isResponseSuccess(finance)
|
||||
? formatTitleCase(
|
||||
finance.data.profit_loss.data.summary.net_profit.label ||
|
||||
'-'
|
||||
)
|
||||
: 'Laba Rugi Netto'}
|
||||
</div>
|
||||
<div className='text-lg font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.net_profit.amount
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
isResponseSuccess(finance)
|
||||
? finance.data.hpp_purchases.title
|
||||
: 'HPP Purchases'
|
||||
}
|
||||
variant='bordered'
|
||||
collapsible
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='mt-6 p-0 mb-0'>
|
||||
<Table<HppTableRow>
|
||||
data={hppTableData}
|
||||
columns={[
|
||||
{
|
||||
header: 'No.',
|
||||
enableSorting: false,
|
||||
accessorFn: (item, index) => {
|
||||
if (item.isGroupHeader) return '-';
|
||||
const dataRowsBefore = hppTableData
|
||||
.slice(0, index)
|
||||
.filter((row) => !row.isGroupHeader).length;
|
||||
return dataRowsBefore + 1;
|
||||
},
|
||||
footer: (props) => {
|
||||
return 'HPP';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Type',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatTitleCase(item.type || '-'),
|
||||
},
|
||||
{
|
||||
header: 'Budgeting',
|
||||
enableSorting: false,
|
||||
columns: [
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
id: 'budgeting_rp_per_bird',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.budgeting?.rp_per_bird || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'budgeting_rp_per_bird' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp.budgeting
|
||||
.rp_per_bird || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Rp/Kg',
|
||||
id: 'budgeting_rp_per_kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.budgeting?.rp_per_kg || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'budgeting_rp_per_kg' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp.budgeting
|
||||
.rp_per_kg || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Jumlah (Rp)',
|
||||
id: 'budgeting_amount',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.budgeting?.amount || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'budgeting_amount' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp.budgeting
|
||||
.amount || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Realization',
|
||||
enableSorting: false,
|
||||
columns: [
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
id: 'realization_rp_per_bird',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.realization?.rp_per_bird || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'realization_rp_per_bird' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp.realization
|
||||
.rp_per_bird || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Rp/Kg',
|
||||
id: 'realization_rp_per_kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.realization?.rp_per_kg || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'realization_rp_per_kg' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp.realization
|
||||
.rp_per_kg || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Jumlah (Rp)',
|
||||
id: 'realization_amount',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) =>
|
||||
formatCurrency(item.realization?.amount || 0),
|
||||
footer: (props) => {
|
||||
return props.column.id === 'realization_amount' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp.realization
|
||||
.amount || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
renderCustomRow={(row) => {
|
||||
const rowData = row.original;
|
||||
if (rowData.isGroupHeader) {
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
||||
>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
></td>
|
||||
<td
|
||||
colSpan={7}
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatTitleCase(rowData.group_name ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
renderFooter={isResponseSuccess(finance)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
isResponseSuccess(finance)
|
||||
? finance.data.profit_loss.title
|
||||
: 'Profit/Loss'
|
||||
}
|
||||
variant='bordered'
|
||||
collapsible
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='mt-6 p-0 mb-0'>
|
||||
<Table<ProfitLossTableRow>
|
||||
data={profitLossTableData}
|
||||
columns={[
|
||||
{
|
||||
header: 'Jenis',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => item.type,
|
||||
cell: (item) => (
|
||||
<div className=''>
|
||||
{formatTitleCase(item.row.original.type || '-')}
|
||||
</div>
|
||||
),
|
||||
footer: (item) => (
|
||||
<div className='font-bold uppercase'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatTitleCase(
|
||||
finance.data.profit_loss.data.summary.net_profit
|
||||
.label || '-'
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
||||
footer: (item) => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.net_profit
|
||||
.rp_per_bird || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Rp/Kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
||||
footer: (item) => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.net_profit
|
||||
.rp_per_kg || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Jumlah (Rp)',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.amount || 0),
|
||||
footer: (item) => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.net_profit
|
||||
.amount || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
renderCustomRow={(row) => {
|
||||
const rowData = row.original;
|
||||
if (rowData.isGroupHeader) {
|
||||
if (rowData.amount) {
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
||||
>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold ps-6 uppercase'>
|
||||
{formatTitleCase(rowData.label ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.amount ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
||||
>
|
||||
<td
|
||||
colSpan={4}
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatTitleCase(rowData.group_name ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
className={{
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
renderFooter={isResponseSuccess(finance)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingFinanceTable;
|
||||
@@ -1,19 +0,0 @@
|
||||
import ClosingOverheadTable from '@/components/pages/closing/ClosingOverheadTable';
|
||||
|
||||
interface ClosingOverheadTabContentProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ClosingOverheadTabContent = ({
|
||||
projectFlockId,
|
||||
}: ClosingOverheadTabContentProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<ClosingOverheadTable projectFlockId={projectFlockId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingOverheadTabContent;
|
||||
@@ -1,162 +0,0 @@
|
||||
import Card from '@/components/Card';
|
||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { Overhead, OverheadTotal } from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
interface ClosingOverheadTableProps {
|
||||
type?: 'detail';
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ClosingOverheadTable = ({
|
||||
type,
|
||||
projectFlockId,
|
||||
}: ClosingOverheadTableProps) => {
|
||||
const { data: overhead, isLoading: isLoadingOverhead } = useSWR(
|
||||
`${ClosingApi.basePath}/${projectFlockId}/overhead`,
|
||||
() => ClosingApi.getOverhead(projectFlockId),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Helper function to create columns with footer support
|
||||
const createColumns = (total?: OverheadTotal): ColumnDef<Overhead>[] => [
|
||||
// Group untuk kolom tanpa footer
|
||||
{
|
||||
header: 'Nama Item',
|
||||
accessorFn: (props) => props.item_name,
|
||||
footer: 'Total Pengeluaran Overhead',
|
||||
},
|
||||
{
|
||||
header: 'Satuan',
|
||||
accessorFn: (props) => props.uom_name,
|
||||
},
|
||||
{
|
||||
header: 'Budget Pengajuan',
|
||||
footer: '',
|
||||
columns: [
|
||||
{
|
||||
id: 'budget_quantity',
|
||||
header: 'Jumlah',
|
||||
accessorFn: (props) =>
|
||||
props.budget_quantity ? formatNumber(props.budget_quantity) : '-',
|
||||
footer: total ? () => formatNumber(total.budget_quantity) : '',
|
||||
},
|
||||
{
|
||||
id: 'budget_unit_price',
|
||||
header: 'Harga Satuan',
|
||||
accessorFn: (props) =>
|
||||
props.budget_unit_price
|
||||
? formatCurrency(props.budget_unit_price)
|
||||
: '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
id: 'budget_total_amount',
|
||||
header: 'Total',
|
||||
accessorFn: (props) =>
|
||||
props.budget_total_amount
|
||||
? formatCurrency(props.budget_total_amount)
|
||||
: '-',
|
||||
footer: total ? () => formatCurrency(total.budget_total_amount) : '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Realisasi',
|
||||
footer: '',
|
||||
columns: [
|
||||
{
|
||||
id: 'actual_date',
|
||||
header: 'Tanggal',
|
||||
accessorFn: (props) =>
|
||||
props.actual_date
|
||||
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
||||
: '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
id: 'actual_quantity',
|
||||
header: 'Jumlah',
|
||||
accessorFn: (props) =>
|
||||
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
||||
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
||||
},
|
||||
{
|
||||
id: 'actual_unit_price',
|
||||
header: 'Harga Satuan',
|
||||
accessorFn: (props) =>
|
||||
props.actual_unit_price
|
||||
? formatCurrency(props.actual_unit_price)
|
||||
: '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
id: 'actual_total_amount',
|
||||
header: 'Total',
|
||||
accessorFn: (props) =>
|
||||
props.actual_total_amount
|
||||
? formatCurrency(props.actual_total_amount)
|
||||
: '-',
|
||||
footer: total ? () => formatCurrency(total.actual_total_amount) : '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cost_per_bird',
|
||||
header: 'Rp/Ekor',
|
||||
accessorFn: (props) =>
|
||||
props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-',
|
||||
footer: total ? () => formatCurrency(total.cost_per_bird) : '',
|
||||
},
|
||||
];
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(overhead)
|
||||
? createColumns(overhead.data?.total)
|
||||
: createColumns(),
|
||||
[overhead]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
title='Pengeluaran Overhead'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Table<Overhead>
|
||||
data={
|
||||
isResponseSuccess(overhead) ? (overhead.data?.overheads ?? []) : []
|
||||
}
|
||||
columns={columns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
headerColumnClassName: cn(
|
||||
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
||||
'whitespace-nowrap'
|
||||
),
|
||||
}}
|
||||
renderFooter={
|
||||
isResponseSuccess(overhead)
|
||||
? overhead.data?.overheads.length > 0
|
||||
: false
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingOverheadTable;
|
||||
@@ -1,235 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import useSWR from 'swr';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
|
||||
interface ClosingProductionDataTabContentProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ClosingProductionDataTabContent = ({
|
||||
projectFlockId,
|
||||
}: ClosingProductionDataTabContentProps) => {
|
||||
const { data: productionData, isLoading } = useSWR(
|
||||
`${ClosingApi.basePath}/${projectFlockId}/production-data`,
|
||||
() => ClosingApi.getProductionData(projectFlockId)
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='w-full flex justify-center py-8'>
|
||||
<span className='loading loading-spinner loading-lg' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!productionData || !isResponseSuccess(productionData)) {
|
||||
return (
|
||||
<div className='w-full text-center py-8 text-gray-500'>
|
||||
Gagal memuat data produksi.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { purchase, sales, performance } = productionData.data;
|
||||
|
||||
// Helper for consistent row styling
|
||||
const DataRow = ({
|
||||
label,
|
||||
value,
|
||||
unit = '',
|
||||
valueClassName = 'font-bold text-gray-800',
|
||||
unitClassName = 'text-gray-500 w-12 text-right',
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
unit?: string;
|
||||
valueClassName?: string;
|
||||
unitClassName?: string;
|
||||
}) => (
|
||||
<div className='flex justify-between items-center py-1'>
|
||||
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
|
||||
<div className='flex gap-2 w-1/2 justify-end items-center'>
|
||||
<span className={valueClassName}>{value}</span>
|
||||
{unit && <span className={unitClassName}>{unit}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='w-full rounded-xl p-8 shadow-sm'>
|
||||
<h2 className='text-lg font-bold mb-8 text-gray-800'>Data Produksi</h2>
|
||||
|
||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
|
||||
{/* Left Column */}
|
||||
<div className='space-y-10'>
|
||||
{/* Purchase Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Pembelian
|
||||
</h3>
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Populasi Awal'
|
||||
value={formatNumber(purchase.initial_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Claim Culling'
|
||||
value={formatNumber(purchase.claim_culling)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Populasi Akhir'
|
||||
value={formatNumber(purchase.final_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Masuk'
|
||||
value={formatNumber(purchase.feed_in)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Terpakai'
|
||||
value={formatNumber(purchase.feed_used)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Terpakai per Ekor'
|
||||
value={formatNumber(purchase.feed_used_per_head)}
|
||||
unit='Kg'
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sales Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Penjualan
|
||||
</h3>
|
||||
<div className='space-y-4'>
|
||||
{/* Chicken Sales */}
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Penjualan (Ekor)'
|
||||
value={formatNumber(sales.chicken.sales_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Penjualan (Kg)'
|
||||
value={formatNumber(sales.chicken.sales_weight)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Bobot Rata-Rata'
|
||||
value={formatNumber(sales.chicken.average_weight)}
|
||||
unit='Kg/Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Harga Jual Rata-Rata'
|
||||
value={formatNumber(
|
||||
sales.chicken.chicken_average_selling_price
|
||||
)}
|
||||
unit='Rupiah'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Egg Sales (if available) */}
|
||||
{sales.egg && (
|
||||
<>
|
||||
<div className='h-px bg-gray-100 my-2' />
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Telur (Butir)'
|
||||
value={formatNumber(sales.egg.egg_pieces)}
|
||||
unit='Butir'
|
||||
/>
|
||||
<DataRow
|
||||
label='Telur (Kg)'
|
||||
value={formatNumber(sales.egg.egg_mass_kg)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Berat Telur Rata-Rata'
|
||||
value={formatNumber(sales.egg.average_egg_weight_kg)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Harga Jual Telur Rata-Rata'
|
||||
value={formatNumber(sales.egg.egg_average_selling_price)}
|
||||
unit='Rupiah'
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Divider Line (Absolute centered) */}
|
||||
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
|
||||
|
||||
{/* Right Column */}
|
||||
<div className='space-y-10 flex flex-col h-full'>
|
||||
{/* Performance Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Performance
|
||||
</h3>
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Deplesi'
|
||||
value={formatNumber(performance.depletion)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Umur'
|
||||
value={formatNumber(performance.age_day)}
|
||||
unit='Hari'
|
||||
/>
|
||||
<DataRow
|
||||
label='Mortalitas Std'
|
||||
value={formatNumber(performance.mortality_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='Mortalitas Act'
|
||||
value={formatNumber(performance.mortality_act)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='DEFF Mortalitas'
|
||||
value={formatNumber(performance.deff_mortality)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='FCR Std'
|
||||
value={formatNumber(performance.fcr_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='FCR Act'
|
||||
value={formatNumber(performance.fcr_act)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='DEFF FCR'
|
||||
value={formatNumber(performance.deff_fcr)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='AWG'
|
||||
value={formatNumber(performance.awg)}
|
||||
unit='Gr/Hari'
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingProductionDataTabContent;
|
||||
@@ -1,25 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
|
||||
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
||||
import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable';
|
||||
|
||||
interface ClosingSapronakCalculationTabContentProps {
|
||||
projectFlockId?: number;
|
||||
}
|
||||
|
||||
const ClosingSapronakCalculationTabContent = ({
|
||||
projectFlockId,
|
||||
}: ClosingSapronakCalculationTabContentProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<>
|
||||
<ClosingSapronakCalculationTable projectFlockId={projectFlockId} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingSapronakCalculationTabContent;
|
||||
@@ -1,229 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
|
||||
import Table from '@/components/Table';
|
||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
RowSapronakCalculation,
|
||||
TotalSapronakCalculation,
|
||||
} from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
|
||||
interface ClosingSapronakCalculationTableProps {
|
||||
type?: 'detail';
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ClosingSapronakCalculationTable = ({
|
||||
type,
|
||||
projectFlockId,
|
||||
}: ClosingSapronakCalculationTableProps) => {
|
||||
const { data: sapronakCalculation, isLoading } = useSWR(
|
||||
`/closing/sapronak-calculation/${projectFlockId}`,
|
||||
() => ClosingApi.getPerhitunganSapronak(projectFlockId),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Helper function to create columns with footer support
|
||||
const createColumns = (
|
||||
total?: TotalSapronakCalculation
|
||||
): ColumnDef<RowSapronakCalculation>[] => [
|
||||
{
|
||||
header: 'Tanggal',
|
||||
accessorKey: 'tanggal',
|
||||
cell: (props) => (props.getValue() as string) || '-',
|
||||
footer: 'Total',
|
||||
},
|
||||
{
|
||||
header: 'No. Referensi',
|
||||
accessorKey: 'no_referensi',
|
||||
cell: (props) => (props.getValue() as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Masuk',
|
||||
accessorKey: 'qty_masuk',
|
||||
cell: (props) => formatNumber(props.getValue() as number),
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{formatNumber(total.qty_masuk)}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Keluar',
|
||||
accessorKey: 'qty_keluar',
|
||||
cell: (props) => formatNumber(props.getValue() as number),
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{formatNumber(total.qty_keluar)}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Pakai',
|
||||
accessorKey: 'qty_pakai',
|
||||
cell: (props) => formatNumber(props.getValue() as number),
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{formatNumber(total.qty_pakai)}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'Uraian',
|
||||
accessorKey: 'uraian',
|
||||
cell: (props) => (props.getValue() as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'Kategori Produk',
|
||||
accessorKey: 'kategori_produk',
|
||||
cell: (props) => (props.getValue() as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'Harga Beli/Qty (Rp)',
|
||||
accessorKey: 'harga_beli_per_qty',
|
||||
cell: (props) => formatCurrency(props.getValue() as number),
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{formatCurrency(total.harga_beli_per_qty)}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'Total Harga (Rp)',
|
||||
accessorKey: 'total_harga',
|
||||
cell: (props) => formatCurrency(props.getValue() as number),
|
||||
footer: total
|
||||
? () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{formatCurrency(total.total_harga)}
|
||||
</div>
|
||||
)
|
||||
: '',
|
||||
},
|
||||
{
|
||||
header: 'Keterangan',
|
||||
accessorKey: 'keterangan',
|
||||
cell: (props) => (props.getValue() as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
];
|
||||
|
||||
// Memoize columns untuk setiap kategori
|
||||
const docBroilerColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.doc_broiler.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
const ovkColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.ovk.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
const pakanColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.pakan.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Card
|
||||
title='DOC Broiler'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? (sapronakCalculation.data?.doc_broiler.rows ?? [])
|
||||
: []
|
||||
}
|
||||
columns={docBroilerColumns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
}}
|
||||
renderFooter={isResponseSuccess(sapronakCalculation)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='OVK'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? (sapronakCalculation.data?.ovk.rows ?? [])
|
||||
: []
|
||||
}
|
||||
columns={ovkColumns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
}}
|
||||
renderFooter={isResponseSuccess(sapronakCalculation)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='Pakan'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? (sapronakCalculation.data?.pakan.rows ?? [])
|
||||
: []
|
||||
}
|
||||
columns={pakanColumns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
}}
|
||||
renderFooter={isResponseSuccess(sapronakCalculation)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingSapronakCalculationTable;
|
||||
@@ -1,110 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import Table from '@/components/Table';
|
||||
import Card from '@/components/Card';
|
||||
import { formatCurrency } from '@/lib/helper';
|
||||
import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing';
|
||||
|
||||
interface HppExpeditionReportTableProps {
|
||||
type?: 'detail';
|
||||
initialValues?: BaseHppExpedition;
|
||||
}
|
||||
|
||||
const HppExpeditionReportTable = ({
|
||||
type = 'detail',
|
||||
initialValues,
|
||||
}: HppExpeditionReportTableProps) => {
|
||||
const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => {
|
||||
return initialValues?.expedition_costs || [];
|
||||
}, [initialValues]);
|
||||
|
||||
const totals = useMemo(() => {
|
||||
const totalHpp = initialValues?.total_hpp_amount || 0;
|
||||
|
||||
return {
|
||||
totalHpp,
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
const costOfRevenueExpeditionColumns: ColumnDef<BaseExpeditionCost>[] =
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'id',
|
||||
accessorKey: 'id',
|
||||
header: 'No',
|
||||
cell: (props) => {
|
||||
return <div>{props.row.index + 1}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
Total HPP Ekspedisi
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'expedition_vendor_name',
|
||||
accessorKey: 'expedition_vendor_name',
|
||||
header: 'Nama Ekspedisi',
|
||||
cell: (props) => props.getValue() || '-',
|
||||
},
|
||||
{
|
||||
id: 'hpp_amount',
|
||||
accessorKey: 'hpp_amount',
|
||||
header: 'HPP Ekspedisi',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(totals.totalHpp)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[totals]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full'>
|
||||
<div className='p-4'>
|
||||
<h2 className='text-xl font-semibold mb-4'>HPP Ekspedisi</h2>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full bg-base-100',
|
||||
body: 'p-0',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
data={costOfRevenueExpeditionData}
|
||||
columns={costOfRevenueExpeditionColumns}
|
||||
renderFooter={costOfRevenueExpeditionData.length > 0}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HppExpeditionReportTable;
|
||||
@@ -0,0 +1,472 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { FinanceApi } from '@/services/api/finance';
|
||||
import { Finance } from '@/types/api/finance';
|
||||
import {
|
||||
BankApi,
|
||||
CustomerApi,
|
||||
KandangApi,
|
||||
SupplierApi,
|
||||
} from '@/services/api/master-data';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Bank } from '@/types/api/master-data/bank';
|
||||
|
||||
type FinanceTableFilter = {
|
||||
search: string;
|
||||
nameSort: string;
|
||||
transactionType: string;
|
||||
customerId: string;
|
||||
supplierId: string;
|
||||
kandangId: string;
|
||||
bankId: string;
|
||||
sortBy: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
|
||||
const TRANSACTION_TYPE_OPTIONS = [
|
||||
{ value: 'REVENUE', label: 'Pemasukan' },
|
||||
{ value: 'EXPENSE', label: 'Pengeluaran' },
|
||||
];
|
||||
|
||||
const SORT_OPTIONS = [
|
||||
{ value: 'payment_date', label: 'Tanggal Pembayaran' },
|
||||
{ value: 'created_date', label: 'Tanggal Dibuat' },
|
||||
];
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
props,
|
||||
}: {
|
||||
type: 'dropdown' | 'collapse';
|
||||
props: CellContext<Finance, unknown>;
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
||||
<Button
|
||||
href={`/finance/edit/${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm'
|
||||
// Implement delete handler later
|
||||
>
|
||||
<Icon icon='mdi:delete-outline' width={16} height={16} />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const FinancesTable = () => {
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
reset: resetFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter<FinanceTableFilter>({
|
||||
initial: {
|
||||
search: '',
|
||||
nameSort: '',
|
||||
transactionType: '',
|
||||
customerId: '',
|
||||
supplierId: '',
|
||||
kandangId: '',
|
||||
bankId: '',
|
||||
sortBy: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
nameSort: 'sort_name',
|
||||
transactionType: 'transaction_type',
|
||||
customerId: 'customer_id',
|
||||
supplierId: 'supplier_id',
|
||||
kandangId: 'kandang_id',
|
||||
bankId: 'bank_id',
|
||||
sortBy: 'sort_by',
|
||||
startDate: 'start_date',
|
||||
endDate: 'end_date',
|
||||
},
|
||||
});
|
||||
|
||||
const { data: finances, isLoading: isLoadingFinances } = useSWR(
|
||||
`${FinanceApi.basePath}${getTableFilterQueryString()}`,
|
||||
FinanceApi.getAllFetcher
|
||||
);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Filter Selection States
|
||||
const [selectedTransactionType, setSelectedTransactionType] =
|
||||
useState<OptionType | null>(null);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [selectedSupplier, setSelectedSupplier] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [selectedBank, setSelectedBank] = useState<OptionType | null>(null);
|
||||
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
|
||||
|
||||
// APIs for SelectInputs
|
||||
const {
|
||||
options: customerOptions,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
setInputValue: setCustomerInputValue,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
options: supplierOptions,
|
||||
isLoadingOptions: isLoadingSupplierOptions,
|
||||
setInputValue: setSupplierInputValue,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
options: kandangOptions,
|
||||
isLoadingOptions: isLoadingKandangOptions,
|
||||
setInputValue: setKandangInputValue,
|
||||
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
options: bankOptions,
|
||||
isLoadingOptions: isLoadingBankOptions,
|
||||
setInputValue: setBankInputValue,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||
|
||||
const financeColumns: ColumnDef<Finance>[] = [
|
||||
{
|
||||
header: '#',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: 'reference_number',
|
||||
header: 'No Referensi',
|
||||
},
|
||||
{
|
||||
accessorKey: 'transaction_type',
|
||||
header: 'Jenis Transaksi',
|
||||
},
|
||||
{
|
||||
accessorKey: 'customer_name',
|
||||
header: 'Pelanggan',
|
||||
},
|
||||
{
|
||||
accessorKey: 'payment_date',
|
||||
header: 'Tanggal Pembayaran',
|
||||
cell: (props) =>
|
||||
formatDate(props.row.original.payment_date, 'DD MMM YYYY'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_date',
|
||||
header: 'Tanggal Dibuat',
|
||||
cell: (props) =>
|
||||
formatDate(props.row.original.created_date, 'DD MMM YYYY'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'payment_method',
|
||||
header: 'Metode Pembayaran',
|
||||
},
|
||||
{
|
||||
accessorKey: 'bank_name',
|
||||
header: 'Bank',
|
||||
},
|
||||
{
|
||||
accessorKey: 'expense_amount',
|
||||
header: 'Pengeluaran',
|
||||
cell: (props) => (
|
||||
<span className='text-error'>
|
||||
{formatCurrency(props.row.original.expense_amount)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'revenue_amount',
|
||||
header: 'Pemasukan',
|
||||
cell: (props) => (
|
||||
<span className='text-success'>
|
||||
{formatCurrency(props.row.original.revenue_amount)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (props) => {
|
||||
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 - 3;
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentPageSize > 3 && (
|
||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||
<RowOptionsMenu type='dropdown' props={props} />
|
||||
</RowDropdownOptions>
|
||||
)}
|
||||
|
||||
{currentPageSize <= 3 && (
|
||||
<RowCollapseOptions>
|
||||
<RowOptionsMenu type='collapse' props={props} />
|
||||
</RowCollapseOptions>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
const handleFilterChange = (
|
||||
value: OptionType | null,
|
||||
setter: (val: OptionType | null) => void,
|
||||
filterKey: keyof FinanceTableFilter
|
||||
) => {
|
||||
setter(value);
|
||||
updateFilter(filterKey, value ? String(value.value) : '');
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setSelectedTransactionType(null);
|
||||
setSelectedCustomer(null);
|
||||
setSelectedSupplier(null);
|
||||
setSelectedKandang(null);
|
||||
setSelectedBank(null);
|
||||
setSelectedSortBy(null);
|
||||
|
||||
resetFilter();
|
||||
};
|
||||
|
||||
// track sorting
|
||||
useEffect(() => {
|
||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||
|
||||
if (!isNameSorted) {
|
||||
updateFilter('nameSort', '');
|
||||
} else {
|
||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||
}
|
||||
}, [sorting, updateFilter]);
|
||||
|
||||
return (
|
||||
<div className='w-full p-0 sm:p-4'>
|
||||
<div className='flex flex-col gap-4 mb-4'>
|
||||
{/* Row 1: Search and Add Button */}
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-4'>
|
||||
<Button
|
||||
href='/finance/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'w-full sm:max-w-xs' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Transaction Type, Customer, Supplier, Mitra */}
|
||||
<div className='grid grid-cols-12 gap-4'>
|
||||
<SelectInput
|
||||
label='Jenis Transaksi'
|
||||
placeholder='Semua Jenis Transaksi'
|
||||
options={TRANSACTION_TYPE_OPTIONS}
|
||||
value={selectedTransactionType}
|
||||
onChange={(val) =>
|
||||
handleFilterChange(
|
||||
val as OptionType,
|
||||
setSelectedTransactionType,
|
||||
'transactionType'
|
||||
)
|
||||
}
|
||||
isClearable
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
|
||||
/>
|
||||
<SelectInput
|
||||
label='Customer'
|
||||
placeholder='Pilih Customer'
|
||||
options={customerOptions}
|
||||
isLoading={isLoadingCustomerOptions}
|
||||
onInputChange={setCustomerInputValue}
|
||||
value={selectedCustomer}
|
||||
onChange={(val) =>
|
||||
handleFilterChange(
|
||||
val as OptionType,
|
||||
setSelectedCustomer,
|
||||
'customerId'
|
||||
)
|
||||
}
|
||||
isClearable
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
|
||||
/>
|
||||
<SelectInput
|
||||
label='Supplier'
|
||||
placeholder='Pilih Supplier'
|
||||
options={supplierOptions}
|
||||
isLoading={isLoadingSupplierOptions}
|
||||
onInputChange={setSupplierInputValue}
|
||||
value={selectedSupplier}
|
||||
onChange={(val) =>
|
||||
handleFilterChange(
|
||||
val as OptionType,
|
||||
setSelectedSupplier,
|
||||
'supplierId'
|
||||
)
|
||||
}
|
||||
isClearable
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
|
||||
/>
|
||||
<SelectInput
|
||||
label='Mitra'
|
||||
placeholder='Pilih Kandang Mitra'
|
||||
options={kandangOptions}
|
||||
isLoading={isLoadingKandangOptions}
|
||||
onInputChange={setKandangInputValue}
|
||||
value={selectedKandang}
|
||||
onChange={(val) =>
|
||||
handleFilterChange(
|
||||
val as OptionType,
|
||||
setSelectedKandang,
|
||||
'kandangId'
|
||||
)
|
||||
}
|
||||
isClearable
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Bank, Sort By, Start Date, End Date */}
|
||||
<div className='grid grid-cols-12 gap-4'>
|
||||
<SelectInput
|
||||
label='Bank'
|
||||
placeholder='Pilih Bank'
|
||||
options={bankOptions}
|
||||
isLoading={isLoadingBankOptions}
|
||||
onInputChange={setBankInputValue}
|
||||
value={selectedBank}
|
||||
onChange={(val) =>
|
||||
handleFilterChange(val as OptionType, setSelectedBank, 'bankId')
|
||||
}
|
||||
isClearable
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
|
||||
/>
|
||||
<SelectInput
|
||||
label='Urutkan Berdasarkan'
|
||||
placeholder='Tanggal Pembayaran'
|
||||
options={SORT_OPTIONS}
|
||||
value={selectedSortBy}
|
||||
onChange={(val) =>
|
||||
handleFilterChange(val as OptionType, setSelectedSortBy, 'sortBy')
|
||||
}
|
||||
isClearable
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
|
||||
/>
|
||||
<DateInput
|
||||
name='startDate'
|
||||
label='Tanggal Mulai'
|
||||
placeholder='Pilih Tanggal Mulai'
|
||||
value={tableFilterState.startDate}
|
||||
onChange={(e) => updateFilter('startDate', e.target.value)}
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
|
||||
/>
|
||||
<DateInput
|
||||
name='endDate'
|
||||
label='Tanggal Akhir '
|
||||
placeholder='Pilih Tanggal Selesai'
|
||||
value={tableFilterState.endDate}
|
||||
onChange={(e) => updateFilter('endDate', e.target.value)}
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-3' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row justify-end items-center'>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Button
|
||||
onClick={resetFilters}
|
||||
variant='outline'
|
||||
color='warning'
|
||||
className='min-w-20'
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table<Finance>
|
||||
data={isResponseSuccess(finances) ? finances?.data : []}
|
||||
columns={financeColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
onPageSizeChange={setPageSize}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
page={isResponseSuccess(finances) ? finances?.meta?.page : 0}
|
||||
totalItems={
|
||||
isResponseSuccess(finances) ? finances?.meta?.total_results : 0
|
||||
}
|
||||
onPageChange={setPage}
|
||||
isLoading={isLoadingFinances}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(finances) && finances?.data?.length === 0,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinancesTable;
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import { CreateFinancePayload } from '@/types/api/finance';
|
||||
|
||||
export const FinanceFormSchema = Yup.object().shape({
|
||||
transactionType: Yup.object()
|
||||
.shape({
|
||||
value: Yup.string().required(),
|
||||
label: Yup.string().required(),
|
||||
})
|
||||
.nullable()
|
||||
.required('Jenis Transaksi Wajib diisi'),
|
||||
customerId: Yup.number()
|
||||
.nullable()
|
||||
.when('transactionType', {
|
||||
is: (val: { value: string }) => val?.value === 'REVENUE',
|
||||
then: (schema) => schema.required('Customer Wajib diisi'),
|
||||
otherwise: (schema) => schema.notRequired(),
|
||||
}),
|
||||
paymentDate: Yup.string().required('Tanggal Pembayaran Wajib diisi'),
|
||||
paymentMethod: Yup.object()
|
||||
.shape({
|
||||
value: Yup.string().required(),
|
||||
label: Yup.string().required(),
|
||||
})
|
||||
.nullable()
|
||||
.required('Metode Pembayaran Wajib diisi'),
|
||||
bankId: Yup.number()
|
||||
.nullable()
|
||||
.test('required-if-transfer', 'Bank Wajib diisi', function (value) {
|
||||
const paymentMethod = this.parent.paymentMethod;
|
||||
if (paymentMethod?.value === 'TRANSFER' && !value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
supplierBankAccountNumber: Yup.string()
|
||||
.nullable()
|
||||
.test(
|
||||
'required-if-transfer',
|
||||
'Nomor Rekening Customer Wajib diisi',
|
||||
function (value) {
|
||||
const paymentMethod = this.parent.paymentMethod;
|
||||
if (paymentMethod?.value === 'TRANSFER' && !value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
),
|
||||
referenceNumber: Yup.string().nullable(),
|
||||
amount: Yup.number()
|
||||
.typeError('Nominal harus berupa angka')
|
||||
.required('Nominal Wajib diisi'),
|
||||
notes: Yup.string().nullable(),
|
||||
});
|
||||
|
||||
export type FinanceFormValues = {
|
||||
transactionType: OptionType | null;
|
||||
customerId: number | null;
|
||||
customer: OptionType | null;
|
||||
paymentDate: string;
|
||||
paymentMethod: OptionType | null;
|
||||
bankId: number | null;
|
||||
bank: OptionType | null;
|
||||
supplierBankAccountNumber: string;
|
||||
referenceNumber: string;
|
||||
amount: string;
|
||||
notes: string;
|
||||
};
|
||||
@@ -0,0 +1,386 @@
|
||||
'use client';
|
||||
|
||||
import { useModal } from '@/components/Modal';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { FinanceApi } from '@/services/api/finance';
|
||||
import {
|
||||
Finance,
|
||||
CreateFinancePayload,
|
||||
UpdateFinancePayload,
|
||||
} from '@/types/api/finance';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import Button from '@/components/Button';
|
||||
import { Icon } from '@iconify/react';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import { cn } from '@/lib/helper';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import TextArea from '@/components/input/TextArea';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import { BankApi, CustomerApi } from '@/services/api/master-data';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { Bank } from '@/types/api/master-data/bank';
|
||||
import {
|
||||
FINANCE_PAYMENT_METHOD_OPTIONS,
|
||||
FINANCE_TRANSACTION_TYPE_OPTIONS,
|
||||
} from '@/config/constant';
|
||||
import { FinanceFormSchema, FinanceFormValues } from './FinanceForm.schema';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface FinanceFormProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
initialValues?: Finance;
|
||||
}
|
||||
|
||||
const FinanceForm = ({ formType = 'add', initialValues }: FinanceFormProps) => {
|
||||
const router = useRouter();
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [financeFormErrorMessage, fileFormErrorMessage] = useState('');
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
// API Options
|
||||
const {
|
||||
options: customerOptions,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
setInputValue: setCustomerInputValue,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
options: bankOptions,
|
||||
isLoadingOptions: isLoadingBankOptions,
|
||||
setInputValue: setBankInputValue,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||
|
||||
const createFinanceHandler = useCallback(
|
||||
async (payload: CreateFinancePayload) => {
|
||||
const createFinanceRes = await FinanceApi.create(payload);
|
||||
|
||||
if (isResponseError(createFinanceRes)) {
|
||||
fileFormErrorMessage(createFinanceRes.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(createFinanceRes?.message as string);
|
||||
router.push('/finance');
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// Note: Update and Delete handlers are not strictly needed for "add" page but good practice to structure for future
|
||||
const deleteFinanceHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const confirmationModalDeleteclickHandler = async () => {
|
||||
// Implement delete logic if needed
|
||||
setIsDeleteLoading(true);
|
||||
// await FinanceApi.delete(initialValues?.id as number);
|
||||
deleteModal.closeModal();
|
||||
setIsDeleteLoading(false);
|
||||
router.push('/finance');
|
||||
};
|
||||
|
||||
const formikInitialValues = useMemo<FinanceFormValues>(() => {
|
||||
return {
|
||||
transactionType: initialValues
|
||||
? {
|
||||
value: initialValues.transaction_type,
|
||||
label: initialValues.transaction_type,
|
||||
} // Should map properly if labels differ
|
||||
: null,
|
||||
customerId: initialValues ? null : null, // ID not directly in BaseFinance without processing, assume add mode mostly
|
||||
customer: initialValues
|
||||
? { value: 0, label: initialValues.customer_name }
|
||||
: null,
|
||||
paymentDate: initialValues?.payment_date ?? '',
|
||||
paymentMethod: প্রাথমিকMethods(initialValues?.payment_method),
|
||||
bankId: null,
|
||||
bank: initialValues ? { value: 0, label: initialValues.bank_name } : null,
|
||||
supplierBankAccountNumber: '',
|
||||
referenceNumber: initialValues?.reference_number ?? '',
|
||||
amount: initialValues
|
||||
? String(initialValues.revenue_amount || initialValues.expense_amount)
|
||||
: '',
|
||||
notes: '',
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
function প্রাথমিকMethods(method?: string) {
|
||||
if (!method) return null;
|
||||
return (
|
||||
FINANCE_PAYMENT_METHOD_OPTIONS.find((o) => o.value === method) ?? {
|
||||
value: method,
|
||||
label: method,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const formik = useFormik<FinanceFormValues>({
|
||||
initialValues: formikInitialValues,
|
||||
validationSchema: FinanceFormSchema,
|
||||
onSubmit: async (values) => {
|
||||
fileFormErrorMessage('');
|
||||
|
||||
const payload: CreateFinancePayload = {
|
||||
transaction_type: (values.transactionType as OptionType)
|
||||
.value as string,
|
||||
customer_id: values.customer ? (values.customer.value as number) : 0,
|
||||
payment_date: values.paymentDate,
|
||||
payment_method: (values.paymentMethod as OptionType).value as string,
|
||||
bank_id: values.bank ? (values.bank.value as number) : 0,
|
||||
supplier_bank_account_number: values.supplierBankAccountNumber,
|
||||
reference_number: values.referenceNumber,
|
||||
amount: Number(values.amount),
|
||||
notes: values.notes,
|
||||
};
|
||||
|
||||
if (formType === 'add') {
|
||||
await createFinanceHandler(payload);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
useEffect(() => {
|
||||
if (formType !== 'add' && initialValues) {
|
||||
// Hydrate logic would go here if editing
|
||||
}
|
||||
}, [formType, initialValues]);
|
||||
|
||||
// Helper for select changes
|
||||
const handleSelectChange = (
|
||||
fieldName: keyof FinanceFormValues,
|
||||
val: OptionType | null
|
||||
) => {
|
||||
formik.setFieldValue(fieldName, val);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full max-w-xl'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/finance'
|
||||
variant='link'
|
||||
className='w-fit p-0 text-primary'
|
||||
>
|
||||
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<h1 className='text-2xl font-bold text-center'>
|
||||
{formType === 'add' && 'Tambah Keuangan'}
|
||||
{formType === 'edit' && 'Ubah Keuangan'}
|
||||
{formType === 'detail' && 'Detail Keuangan'}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
className='w-full mt-8 flex flex-col gap-6'
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<SelectInput
|
||||
required
|
||||
label='Pilih Jenis Transaksi'
|
||||
placeholder='Pilih Jenis Transaksi'
|
||||
options={FINANCE_TRANSACTION_TYPE_OPTIONS}
|
||||
value={formik.values.transactionType}
|
||||
onChange={(val) =>
|
||||
handleSelectChange('transactionType', val as OptionType)
|
||||
}
|
||||
isError={
|
||||
formik.touched.transactionType &&
|
||||
Boolean(formik.errors.transactionType)
|
||||
}
|
||||
errorMessage={formik.errors.transactionType as string}
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
required
|
||||
label='Customer'
|
||||
placeholder='Pilih Customer'
|
||||
options={customerOptions}
|
||||
isLoading={isLoadingCustomerOptions}
|
||||
onInputChange={setCustomerInputValue}
|
||||
value={formik.values.customer}
|
||||
onChange={(val) =>
|
||||
handleSelectChange('customer', val as OptionType)
|
||||
}
|
||||
isError={
|
||||
formik.touched.customer && Boolean(formik.errors.customer)
|
||||
}
|
||||
errorMessage={formik.errors.customer as string} // Schema logic handles this validation conditionally
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
required
|
||||
label='Tanggal Pembayaran'
|
||||
name='paymentDate'
|
||||
placeholder='Pilih Tanggal Pembayaran'
|
||||
value={formik.values.paymentDate}
|
||||
onChange={(e) =>
|
||||
formik.setFieldValue('paymentDate', e.target.value)
|
||||
}
|
||||
isError={
|
||||
formik.touched.paymentDate && Boolean(formik.errors.paymentDate)
|
||||
}
|
||||
errorMessage={formik.errors.paymentDate}
|
||||
readOnly={formType === 'detail'}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
required
|
||||
label='Metode Pembayaran'
|
||||
placeholder='Pilih Metode Pembayaran'
|
||||
options={FINANCE_PAYMENT_METHOD_OPTIONS}
|
||||
value={formik.values.paymentMethod}
|
||||
onChange={(val) =>
|
||||
handleSelectChange('paymentMethod', val as OptionType)
|
||||
}
|
||||
isError={
|
||||
formik.touched.paymentMethod &&
|
||||
Boolean(formik.errors.paymentMethod)
|
||||
}
|
||||
errorMessage={formik.errors.paymentMethod as string}
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Nomor Rekening'
|
||||
placeholder='Pilih Bank'
|
||||
options={bankOptions}
|
||||
isLoading={isLoadingBankOptions}
|
||||
onInputChange={setBankInputValue}
|
||||
value={formik.values.bank}
|
||||
onChange={(val) => handleSelectChange('bank', val as OptionType)}
|
||||
isError={formik.touched.bank && Boolean(formik.errors.bank)} // Actually controlled by bankId in schema but logic applies
|
||||
errorMessage={formik.errors.bankId as string} // bankId error mapping
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
required
|
||||
label='Nomor Rekening Customer' // As per screenshot
|
||||
name='supplierBankAccountNumber'
|
||||
placeholder='Nomor Rekening'
|
||||
value={formik.values.supplierBankAccountNumber}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.supplierBankAccountNumber &&
|
||||
Boolean(formik.errors.supplierBankAccountNumber)
|
||||
}
|
||||
errorMessage={formik.errors.supplierBankAccountNumber}
|
||||
readOnly={formType === 'detail'}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Nomor Referensi'
|
||||
name='referenceNumber'
|
||||
placeholder='Nomor Referensi'
|
||||
value={formik.values.referenceNumber}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.referenceNumber &&
|
||||
Boolean(formik.errors.referenceNumber)
|
||||
}
|
||||
errorMessage={formik.errors.referenceNumber}
|
||||
readOnly={formType === 'detail'}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Nominal'
|
||||
name='amount'
|
||||
placeholder='Nominal'
|
||||
value={Number(formik.values.amount)}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={formik.touched.amount && Boolean(formik.errors.amount)}
|
||||
errorMessage={formik.errors.amount}
|
||||
readOnly={formType === 'detail'}
|
||||
inputPrefix={
|
||||
<span className='text-gray-600 font-medium'>Rp</span>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
required
|
||||
label='Catatan'
|
||||
name='notes'
|
||||
placeholder='Catatan'
|
||||
value={formik.values.notes}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||
errorMessage={formik.errors.notes}
|
||||
readOnly={formType === 'detail'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Button
|
||||
type='submit'
|
||||
color='primary'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={!formik.isValid || !formik.dirty || formik.isSubmitting}
|
||||
className='w-24'
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='warning'
|
||||
href='/finance'
|
||||
className='w-24'
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
</div>
|
||||
{financeFormErrorMessage && (
|
||||
<div role='alert' className='alert alert-error'>
|
||||
<Icon
|
||||
icon='material-symbols:error-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{financeFormErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
{formType !== 'add' && (
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
onClick: confirmationModalDeleteclickHandler,
|
||||
isLoading: isDeleteLoading,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinanceForm;
|
||||
@@ -1,10 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import Badge from '@/components/Badge';
|
||||
import Button from '@/components/Button';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import Table from '@/components/Table';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn } from '@/lib/helper';
|
||||
@@ -79,39 +77,46 @@ const InventoryAdjustmentTable = () => {
|
||||
year: 'numeric',
|
||||
}),
|
||||
},
|
||||
// {
|
||||
// id: 'before_quantity',
|
||||
// header: 'Stok Sebelum',
|
||||
// accessorFn: (row) =>
|
||||
// formatNumber(String(row.product_warehouse?.quantity)),
|
||||
// },
|
||||
// {
|
||||
// id: 'after_quantity',
|
||||
// header: 'Stok Sesudah',
|
||||
// accessorFn: (row) =>
|
||||
// formatNumber(String(row.product_warehouse?.quantity)),
|
||||
// },
|
||||
{
|
||||
id: 'before_quantity',
|
||||
header: 'Stok Sebelum',
|
||||
accessorFn: (row) => formatNumber(String(row.before_quantity)),
|
||||
},
|
||||
{
|
||||
id: 'after_quantity',
|
||||
header: 'Stok Sesudah',
|
||||
accessorFn: (row) => formatNumber(String(row.after_quantity)),
|
||||
},
|
||||
{
|
||||
id: 'quantity',
|
||||
header: 'Kuantitas',
|
||||
accessorFn: (row) => formatNumber(String(row.increase + row.decrease)),
|
||||
accessorFn: (row) => formatNumber(String(row.quantity)),
|
||||
},
|
||||
{
|
||||
id: 'transaction_type',
|
||||
header: 'Tipe Transaksi',
|
||||
accessorFn: (row) => {
|
||||
if (row.increase > 0) return 'Peningkatan';
|
||||
if (row.decrease > 0) return 'Penurunan';
|
||||
if (row.transaction_type === 'INCREASE') return 'Peningkatan';
|
||||
if (row.transaction_type === 'DECREASE') return 'Penurunan';
|
||||
return '-';
|
||||
},
|
||||
cell: (props) => {
|
||||
const type = props.row.original.increase;
|
||||
const label = type > 0 ? 'Peningkatan' : type <= 0 ? 'Penurunan' : '-';
|
||||
const type = props.row.original.transaction_type;
|
||||
const label =
|
||||
type === 'INCREASE'
|
||||
? 'Peningkatan'
|
||||
: type === 'DECREASE'
|
||||
? 'Penurunan'
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<Badge variant='soft' color={type > 0 ? 'success' : 'error'}>
|
||||
<div
|
||||
className={`small mx-auto badge badge-soft ${
|
||||
type === 'INCREASE' ? 'badge-success' : 'badge-error'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -176,17 +181,15 @@ const InventoryAdjustmentTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.inventory.create'>
|
||||
<Button
|
||||
href='/inventory/adjustment/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/inventory/adjustment/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
|
||||
{/* <DebouncedTextInput
|
||||
name='search'
|
||||
|
||||
@@ -76,7 +76,7 @@ const InventoryAdjustmentForm = ({
|
||||
product_category: undefined,
|
||||
product: undefined,
|
||||
warehouse: undefined,
|
||||
quantity: initialValues?.increase ?? initialValues?.decrease ?? 0,
|
||||
quantity: initialValues?.quantity ?? 0,
|
||||
transaction_type: undefined,
|
||||
note: initialValues?.note ?? '',
|
||||
};
|
||||
@@ -214,8 +214,16 @@ const InventoryAdjustmentForm = ({
|
||||
'quantity',
|
||||
initialValues.product_warehouse.quantity
|
||||
);
|
||||
formik.setFieldValue(
|
||||
'transaction_type',
|
||||
initialValues.transaction_type.toLowerCase()
|
||||
);
|
||||
formik.setFieldValue('note', initialValues.note);
|
||||
}
|
||||
if (initialValues?.transaction_type) {
|
||||
const type = initialValues.transaction_type.toLowerCase();
|
||||
setQuantityLabel(type === 'increase' ? 'Tambah Stok' : 'Kurangi Stok');
|
||||
}
|
||||
}, [
|
||||
formik,
|
||||
initialValues,
|
||||
@@ -270,6 +278,26 @@ const InventoryAdjustmentForm = ({
|
||||
className='w-full mt-8 flex flex-col gap-6'
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
{/* Text Input Before Quantity */}
|
||||
{type === 'detail' && initialValues && (
|
||||
<>
|
||||
<TextInput
|
||||
label='Stok Sebelum'
|
||||
name='before_quantity'
|
||||
type='text'
|
||||
value={formatNumber(String(initialValues.before_quantity))}
|
||||
readOnly={true}
|
||||
/>
|
||||
<TextInput
|
||||
label='Stok Setelah'
|
||||
name='after_quantity'
|
||||
type='text'
|
||||
readOnly={true}
|
||||
value={formatNumber(String(initialValues.after_quantity))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Select Input Product Category */}
|
||||
<SelectInput
|
||||
required
|
||||
|
||||
@@ -19,7 +19,6 @@ import SelectInput from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
@@ -29,17 +28,15 @@ const RowOptionsMenu = ({
|
||||
props: CellContext<Movement, unknown>;
|
||||
}) => (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.inventory.transfer.detail'>
|
||||
<Button
|
||||
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
|
||||
@@ -148,17 +145,15 @@ const MovementTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row gap-2'>
|
||||
<RequirePermission permissions='lti.inventory.transfer.create'>
|
||||
<Button
|
||||
href='/inventory/movement/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/inventory/movement/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -7,7 +7,6 @@ import Table from '@/components/Table';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
@@ -32,17 +31,15 @@ const RowOptionsMenu = ({
|
||||
props: CellContext<InventoryProduct, unknown>;
|
||||
}) => (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.inventory.product_stock.detail'>
|
||||
<Button
|
||||
href={`/inventory/product/detail?inventoryProductId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/inventory/product/detail?inventoryProductId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
|
||||
|
||||
@@ -13,12 +13,8 @@ const InventoryProductDetail = ({
|
||||
}) => {
|
||||
const stockLogs = useMemo(() => {
|
||||
return (
|
||||
inventoryProduct?.product_warehouses?.flatMap((warehouse) =>
|
||||
warehouse.stock_logs.map((log) => ({
|
||||
...log,
|
||||
warehouse_name: warehouse.warehouse_name,
|
||||
warehouse_id: warehouse.warehouse_id,
|
||||
}))
|
||||
inventoryProduct?.product_warehouses?.flatMap(
|
||||
(warehouse) => warehouse.stock_logs || []
|
||||
) || []
|
||||
);
|
||||
}, [inventoryProduct]);
|
||||
|
||||
@@ -3,11 +3,7 @@ import Table from '@/components/Table';
|
||||
import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
|
||||
import { StockLog } from '@/types/api/inventory/product';
|
||||
|
||||
const StockLogTable = ({
|
||||
stockLogs,
|
||||
}: {
|
||||
stockLogs: (StockLog & { warehouse_name: string; warehouse_id: number })[];
|
||||
}) => {
|
||||
const StockLogTable = ({ stockLogs }: { stockLogs: StockLog[] }) => {
|
||||
return (
|
||||
<Card
|
||||
title='Informasi Stock Produk'
|
||||
@@ -31,10 +27,6 @@ const StockLogTable = ({
|
||||
return formatDate(props.row.original.created_at, 'DD-MMM-yyyy');
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Gudang',
|
||||
accessorKey: 'warehouse_name',
|
||||
},
|
||||
{
|
||||
header: 'Peningkatan',
|
||||
accessorKey: 'increase',
|
||||
|
||||
@@ -15,7 +15,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import { AreaApi } from '@/services/api/master-data';
|
||||
@@ -35,46 +34,40 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.area.detail'>
|
||||
<Button
|
||||
href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.area.update'>
|
||||
<Button
|
||||
href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.area.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='text-error hover:text-inherit'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='text-error hover:text-inherit'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -199,19 +192,15 @@ const AreasTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.area.create'>
|
||||
<Button
|
||||
href='/master-data/area/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
<Button
|
||||
href='/master-data/area/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -10,7 +10,6 @@ import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
AreaFormSchema,
|
||||
@@ -161,40 +160,36 @@ const AreaForm = ({ type = 'add', initialValues }: AreaFormProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.area.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteAreaClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteAreaClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/area/detail/edit/?areaId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.area.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/area/detail/edit/?areaId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Bank } from '@/types/api/master-data/bank';
|
||||
import { BankApi } from '@/services/api/master-data';
|
||||
@@ -35,46 +34,40 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.banks.detail'>
|
||||
<Button
|
||||
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.banks.update'>
|
||||
<Button
|
||||
href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.banks.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -212,17 +205,15 @@ const BanksTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.banks.create'>
|
||||
<Button
|
||||
href='/master-data/bank/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/bank/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -10,7 +10,6 @@ import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
BankFormSchema,
|
||||
@@ -209,40 +208,36 @@ const BankForm = ({ type = 'add', initialValues }: BankFormProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.banks.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteBankClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteBankClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/bank/detail/edit/?bankId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.banks.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/bank/detail/edit/?bankId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,6 @@ import Table from '@/components/Table';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn } from '@/lib/helper';
|
||||
@@ -33,44 +32,38 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.customer.detail'>
|
||||
<Button
|
||||
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
<Button
|
||||
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.master.customer.update'>
|
||||
<Button
|
||||
href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.master.customer.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -207,17 +200,15 @@ const CustomersTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.customer.create'>
|
||||
<Button
|
||||
href='/master-data/customer/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/customer/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -27,7 +27,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import useSWR from 'swr';
|
||||
import { UserApi } from '@/services/api/user';
|
||||
import { TYPE_OPTIONS } from '@/config/constant';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
interface CustomerFormProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -320,40 +319,36 @@ const CustomerForm = ({
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{formType !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.customer.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteCustomerHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{formType !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteCustomerHandler}
|
||||
color='warning'
|
||||
href={`/master-data/customer/detail/edit/?customerId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{formType !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.customer.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/customer/detail/edit/?customerId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Fcr } from '@/types/api/master-data/fcr';
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
@@ -35,46 +34,40 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.fcr.detail'>
|
||||
<Button
|
||||
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.fcr.update'>
|
||||
<Button
|
||||
href={`/master-data/fcr/detail/edit/?fcrId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/fcr/detail/edit/?fcrId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.fcr.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -199,17 +192,15 @@ const FcrsTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.fcr.create'>
|
||||
<Button
|
||||
href='/master-data/fcr/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/fcr/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -10,7 +10,6 @@ import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
FcrFormSchema,
|
||||
@@ -297,40 +296,36 @@ const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.fcr.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteFcrClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteFcrClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/fcr/detail/edit/?fcrId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.fcr.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/fcr/detail/edit/?fcrId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useModal } from '@/components/Modal';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import toast from 'react-hot-toast';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
@@ -33,54 +32,48 @@ const RowsOptions = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.flocks.update'>
|
||||
<Button
|
||||
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
<Button
|
||||
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.master.flocks.detail'>
|
||||
<Button
|
||||
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:eye-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:eye-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.master.flocks.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
/>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -203,17 +196,15 @@ const FlockTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.flocks.create'>
|
||||
<Button
|
||||
href='/master-data/flock/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/flock/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Icon } from '@iconify/react';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { cn } from '@/lib/helper';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
interface FlockCustomProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -131,39 +130,35 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{formType !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.flocks.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => deleteModal.openModal()}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
{formType !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => deleteModal.openModal()}
|
||||
color='warning'
|
||||
href={`/master-data/flock/detail/edit/?flockId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
{formType !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.flocks.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/flock/detail/edit/?flockId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -20,7 +20,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { KandangApi } from '@/services/api/master-data';
|
||||
@@ -40,46 +39,40 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.kandangs.detail'>
|
||||
<Button
|
||||
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.kandangs.update'>
|
||||
<Button
|
||||
href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.kandangs.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -250,19 +243,15 @@ const KandangsTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.kandangs.create'>
|
||||
<Button
|
||||
href='/master-data/kandang/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
<Button
|
||||
href='/master-data/kandang/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -12,7 +12,6 @@ import TextInput from '@/components/input/TextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
KandangFormSchema,
|
||||
@@ -286,40 +285,36 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.kandangs.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteKandangClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteKandangClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/kandang/detail/edit/?kandangId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.kandangs.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/kandang/detail/edit/?kandangId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -20,7 +20,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
@@ -40,46 +39,40 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.locations.detail'>
|
||||
<Button
|
||||
href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.locations.update'>
|
||||
<Button
|
||||
href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.locations.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -237,19 +230,15 @@ const LocationsTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.locations.create'>
|
||||
<Button
|
||||
href='/master-data/location/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
<Button
|
||||
href='/master-data/location/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -12,7 +12,6 @@ import TextInput from '@/components/input/TextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
LocationFormSchema,
|
||||
@@ -230,40 +229,36 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.locations.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteLocationClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteLocationClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/location/detail/edit/?locationId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.locations.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/location/detail/edit/?locationId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -20,7 +20,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||
import { NonstockApi } from '@/services/api/master-data';
|
||||
@@ -40,46 +39,40 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.nonstocks.detail'>
|
||||
<Button
|
||||
href={`/master-data/nonstock/detail/?nonstockId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/nonstock/detail/?nonstockId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.nonstocks.update'>
|
||||
<Button
|
||||
href={`/master-data/nonstock/detail/edit/?nonstockId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/nonstock/detail/edit/?nonstockId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.nonstocks.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -249,17 +242,15 @@ const NonstocksTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.nonstocks.create'>
|
||||
<Button
|
||||
href='/master-data/nonstock/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/nonstock/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -12,7 +12,6 @@ import TextInput from '@/components/input/TextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
NonstockFormSchema,
|
||||
@@ -299,40 +298,36 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.nonstocks.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteNonstockClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteNonstockClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/nonstock/detail/edit/?nonstockId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.nonstocks.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/nonstock/detail/edit/?nonstockId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||
@@ -35,46 +34,38 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.product_categories.detail'>
|
||||
<Button
|
||||
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
<Button
|
||||
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:delete-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
<RequirePermission permissions='lti.master.product_categories.update'>
|
||||
<Button
|
||||
href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
<RequirePermission permissions='lti.master.product_categories.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:delete-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -202,17 +193,15 @@ const ProductCategoryTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.product_categories.create'>
|
||||
<Button
|
||||
href='/master-data/product-category/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/product-category/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
|
||||
@@ -10,7 +10,6 @@ import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
ProductCategoryFormSchema,
|
||||
@@ -184,40 +183,36 @@ const ProductCategoryForm = ({
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.product_categories.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteProductCategoryClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteProductCategoryClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/product-category/detail/edit/?productCategoryId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.product_categories.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/product-category/detail/edit/?productCategoryId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -20,7 +20,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { ProductApi } from '@/services/api/master-data';
|
||||
@@ -39,44 +38,38 @@ const RowOptionsMenu = ({
|
||||
deleteClickHandler: () => void;
|
||||
}) => (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.products.detail'>
|
||||
<Button
|
||||
href={`/master-data/product/detail/?productId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
<Button
|
||||
href={`/master-data/product/detail/?productId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href={`/master-data/product/detail/edit/?productId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.master.products.update'>
|
||||
<Button
|
||||
href={`/master-data/product/detail/edit/?productId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.master.products.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
|
||||
@@ -280,17 +273,15 @@ const ProductsTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.products.create'>
|
||||
<Button
|
||||
href='/master-data/product/add'
|
||||
variant='outline'
|
||||
className='w-full sm:w-fit'
|
||||
color='primary'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/product/add'
|
||||
variant='outline'
|
||||
className='w-full sm:w-fit'
|
||||
color='primary'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
|
||||
@@ -16,7 +16,6 @@ import SelectInput, {
|
||||
} from '@/components/input/SelectInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
ProductFormSchema,
|
||||
@@ -414,39 +413,35 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.products.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteProductClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteProductClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/product/detail/edit/?productId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.products.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/product/detail/edit/?productId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,6 @@ import Table from '@/components/Table';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn } from '@/lib/helper';
|
||||
@@ -33,54 +32,48 @@ const RowOptions = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.suppliers.detail'>
|
||||
<Button
|
||||
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
<Button
|
||||
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:eye-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:eye-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.master.suppliers.update'>
|
||||
<Button
|
||||
href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
/>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.master.suppliers.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -226,17 +219,15 @@ const SuppliersTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.suppliers.create'>
|
||||
<Button
|
||||
href='/master-data/supplier/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/supplier/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -24,7 +24,6 @@ import TextInput from '@/components/input/TextInput';
|
||||
import TextArea from '@/components/input/TextArea';
|
||||
import { cn } from '@/lib/helper';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
interface SupplierCustomProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -407,40 +406,36 @@ const SupplierForm = ({
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{formType !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.suppliers.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteSupplierHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{formType !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteSupplierHandler}
|
||||
color='warning'
|
||||
href={`/master-data/supplier/detail/edit/?supplierId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{formType !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.suppliers.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/supplier/detail/edit/?supplierId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Uom } from '@/types/api/master-data/uom';
|
||||
import { UomApi } from '@/services/api/master-data';
|
||||
@@ -35,46 +34,40 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.uoms.detail'>
|
||||
<Button
|
||||
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.uoms.update'>
|
||||
<Button
|
||||
href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.uoms.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -199,17 +192,15 @@ const UomsTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.uoms.create'>
|
||||
<Button
|
||||
href='/master-data/uom/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/uom/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -10,7 +10,6 @@ import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
UomFormSchema,
|
||||
@@ -161,40 +160,36 @@ const UomForm = ({ type = 'add', initialValues }: UomFormProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.uoms.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteUomClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteUomClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/uom/detail/edit/?uomId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.uoms.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/uom/detail/edit/?uomId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -20,7 +20,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { WarehouseApi } from '@/services/api/master-data';
|
||||
@@ -40,46 +39,40 @@ const RowOptionsMenu = ({
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.warehouses.detail'>
|
||||
<Button
|
||||
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.warehouses.update'>
|
||||
<Button
|
||||
href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<RequirePermission permissions='lti.master.warehouses.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='text-error hover:text-inherit'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='text-error hover:text-inherit'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
@@ -277,17 +270,15 @@ const WarehousesTable = () => {
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.warehouses.create'>
|
||||
<Button
|
||||
href='/master-data/warehouse/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<Button
|
||||
href='/master-data/warehouse/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -12,7 +12,6 @@ import TextInput from '@/components/input/TextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
WarehouseFormSchema,
|
||||
@@ -436,40 +435,36 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.warehouses.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteWarehouseClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteWarehouseClickHandler}
|
||||
color='warning'
|
||||
href={`/master-data/warehouse/detail/edit/?warehouseId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.warehouses.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/warehouse/detail/edit/?warehouseId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,12 +11,12 @@ import { useState } from 'react';
|
||||
import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import { PROJECT_FLOCK_KANDANG_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import ChickinFormView from '@/components/pages/production/chickin/form/tabs/ChickinFormView';
|
||||
import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/ChickLogsView';
|
||||
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Badge from '@/components/Badge';
|
||||
import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line';
|
||||
const ChickinFormKandang = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
@@ -34,8 +34,8 @@ const ChickinFormKandang = ({
|
||||
refresh: refreshApprovals,
|
||||
} = useApprovalSteps({
|
||||
latestApproval: initialValues?.approval,
|
||||
approvalLines: CHICKINS_APPROVAL_LINE,
|
||||
moduleName: 'CHICKINS',
|
||||
approvalLines: PROJECT_FLOCK_KANDANG_APPROVAL_LINE,
|
||||
moduleName: 'PROJECT_FLOCK_KANDANGS',
|
||||
moduleId: initialValues?.id.toString() ?? '',
|
||||
});
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ const ChickinLogsView = ({
|
||||
})
|
||||
)}
|
||||
|
||||
{initialValues?.approval?.step_number <= 2 && (
|
||||
{initialValues?.approval?.step_number == 1 && (
|
||||
<Button
|
||||
color='success'
|
||||
onClick={handleClickApprove}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import {
|
||||
ClosingExpense,
|
||||
ProjectFlockKandang,
|
||||
StockItem,
|
||||
} from '@/types/api/production/project-flock-kandang';
|
||||
import { Icon } from '@iconify/react';
|
||||
import useSWR from 'swr';
|
||||
@@ -21,7 +20,6 @@ import { useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
import { ApprovalApi } from '@/services/api/approval';
|
||||
|
||||
const ProjectFlockClosingForm = ({
|
||||
projectFlock,
|
||||
@@ -32,7 +30,7 @@ const ProjectFlockClosingForm = ({
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const closeModal = useModal();
|
||||
|
||||
const isCanClose = projectFlock.approval?.step_number <= 2;
|
||||
const [isClosingLoading, setIsClosingLoading] = useState(false);
|
||||
|
||||
const { data: closingData, isLoading } = useSWR(
|
||||
@@ -40,35 +38,19 @@ const ProjectFlockClosingForm = ({
|
||||
() => ProjectFlockKandangApi.checkClosing(projectFlockKandang.id)
|
||||
);
|
||||
|
||||
const { data: projectFlockKandangApprovals } = useSWR(
|
||||
`${ApprovalApi.basePath}?module_name=PROJECT_FLOCK_KANDANGS&module_id=${projectFlockKandang.id}`,
|
||||
() =>
|
||||
ApprovalApi.getAllFetcher(
|
||||
`${ApprovalApi.basePath}?module_name=PROJECT_FLOCK_KANDANGS&module_id=${projectFlockKandang.id}`
|
||||
)
|
||||
);
|
||||
|
||||
const isCanClose = useMemo(() => {
|
||||
return isResponseSuccess(projectFlockKandangApprovals)
|
||||
? projectFlockKandangApprovals?.data?.[0]?.step_number <= 2
|
||||
: true;
|
||||
}, [projectFlockKandangApprovals]);
|
||||
|
||||
const confirmationModalCloseClickHandler = async () => {
|
||||
setIsClosingLoading(true);
|
||||
const deleteProjectFlockRes = await ProjectFlockKandangApi.closing(
|
||||
projectFlockKandang?.id as number,
|
||||
projectFlock?.id as number,
|
||||
{
|
||||
closed_date: isCanClose ? formatDate(new Date(), 'YYYY-MM-DD') : '',
|
||||
closed_date: formatDate(new Date(), 'yyyy-MM-dd'),
|
||||
action: isCanClose ? 'close' : 'unclose',
|
||||
}
|
||||
);
|
||||
|
||||
if (isResponseSuccess(deleteProjectFlockRes)) {
|
||||
toast.success(deleteProjectFlockRes?.message as string);
|
||||
router.push(
|
||||
`/production/project-flock/detail?projectFlockId=${projectFlock.id}`
|
||||
);
|
||||
router.push(`/production/project-flock`);
|
||||
}
|
||||
if (isResponseError(deleteProjectFlockRes)) {
|
||||
toast.error(deleteProjectFlockRes?.message as string);
|
||||
@@ -80,16 +62,16 @@ const ProjectFlockClosingForm = ({
|
||||
const errorStock = useMemo(() => {
|
||||
return isResponseSuccess(closingData)
|
||||
? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0)
|
||||
: true;
|
||||
: false;
|
||||
}, [closingData]);
|
||||
|
||||
const errorExpense = useMemo(() => {
|
||||
return isResponseSuccess(closingData)
|
||||
? closingData?.data?.expenses.every((expense) => expense.step < 5)
|
||||
: true;
|
||||
: false;
|
||||
}, [closingData]);
|
||||
|
||||
const isCanCloseValid = true;
|
||||
const isCanCloseValid = !errorStock && !errorExpense;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -210,7 +192,7 @@ const ProjectFlockClosingForm = ({
|
||||
: 'error'
|
||||
}
|
||||
>
|
||||
{formatTitleCase(props.row.original.step_name)}
|
||||
{formatTitleCase(props.row.original.status)}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
@@ -229,18 +211,18 @@ const ProjectFlockClosingForm = ({
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
{/* {errorExpense && (
|
||||
<div className='text-center text-error text-sm'>
|
||||
{errorExpense && (
|
||||
<div className='text-center text-error'>
|
||||
*Pastikan semua biaya sudah selesai sebelum melakukan closing.
|
||||
</div>
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Table Persediaan Gudang */}
|
||||
<div className='divider'></div>
|
||||
<div className='px-4 pb-4'>
|
||||
<h2 className='text-2xl font-semibold'>Persediaan Gudang</h2>
|
||||
<Table<StockItem>
|
||||
<Table<ProductWarehouse>
|
||||
data={
|
||||
isResponseSuccess(closingData)
|
||||
? closingData.data?.stock_remaining
|
||||
@@ -249,11 +231,11 @@ const ProjectFlockClosingForm = ({
|
||||
columns={[
|
||||
{
|
||||
header: 'Product',
|
||||
accessorKey: 'product_name',
|
||||
accessorKey: 'product.name',
|
||||
},
|
||||
{
|
||||
header: 'Kategori',
|
||||
accessorKey: 'product_category',
|
||||
accessorKey: 'product.product_category.name',
|
||||
},
|
||||
{
|
||||
header: 'Quantity',
|
||||
@@ -261,7 +243,7 @@ const ProjectFlockClosingForm = ({
|
||||
},
|
||||
{
|
||||
header: 'UOM',
|
||||
accessorKey: 'uom',
|
||||
accessorKey: 'product.uom.name',
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
@@ -277,11 +259,11 @@ const ProjectFlockClosingForm = ({
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
{/* {errorStock && (
|
||||
<div className='text-center text-error text-sm'>
|
||||
{errorStock && (
|
||||
<div className='text-center text-error'>
|
||||
*Masih ada sisa stock yang belum dihabiskan.
|
||||
</div>
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='p-4 mt-6'>
|
||||
|
||||
@@ -23,12 +23,7 @@ import toast from 'react-hot-toast';
|
||||
import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import {
|
||||
PROJECT_FLOCK_APPROVAL_LINE,
|
||||
PROJECT_FLOCK_KANDANGS_APPROVAL_LINE,
|
||||
} from '@/config/approval-line';
|
||||
import useSWR from 'swr';
|
||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
|
||||
|
||||
const ProjectFlockDetail = ({
|
||||
projectFlock,
|
||||
@@ -43,23 +38,10 @@ const ProjectFlockDetail = ({
|
||||
null
|
||||
);
|
||||
|
||||
const selectedKandang = projectFlock.kandangs?.find(
|
||||
const selectedKandang = projectFlock.kandangs.find(
|
||||
(kandang) => kandang.id === Number(selectedKandangId)
|
||||
);
|
||||
|
||||
const { data: projectFlockKandang, isLoading: projectFlockKandangLoading } =
|
||||
useSWR(
|
||||
selectedKandangId
|
||||
? `${ProjectFlockKandangApi.basePath}/get-detail/${selectedKandangId}`
|
||||
: null,
|
||||
selectedKandangId
|
||||
? () =>
|
||||
ProjectFlockKandangApi.getSingle(
|
||||
Number(selectedKandang?.project_flock_kandang_id)
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const {
|
||||
approvals,
|
||||
isLoading: approvalsLoading,
|
||||
@@ -68,23 +50,9 @@ const ProjectFlockDetail = ({
|
||||
latestApproval: projectFlock?.approval,
|
||||
approvalLines: PROJECT_FLOCK_APPROVAL_LINE,
|
||||
moduleName: 'PROJECT_FLOCKS',
|
||||
moduleId: projectFlock?.id?.toString() ?? '',
|
||||
moduleId: projectFlock?.id.toString() ?? '',
|
||||
});
|
||||
|
||||
const { approvals: kandangApprovals, isLoading: kandangApprovalsLoading } =
|
||||
useApprovalSteps({
|
||||
latestApproval:
|
||||
selectedKandangId && isResponseSuccess(projectFlockKandang)
|
||||
? projectFlockKandang?.data?.approval
|
||||
: undefined,
|
||||
approvalLines: PROJECT_FLOCK_KANDANGS_APPROVAL_LINE,
|
||||
moduleName: 'PROJECT_FLOCK_KANDANGS',
|
||||
moduleId:
|
||||
selectedKandangId && isResponseSuccess(projectFlockKandang)
|
||||
? projectFlockKandang?.data?.id?.toString()
|
||||
: '',
|
||||
});
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
const deleteProjectFlockRes = await ProjectFlockApi.delete(
|
||||
@@ -183,7 +151,7 @@ const ProjectFlockDetail = ({
|
||||
className={{ badge: 'rounded-lg px-2' }}
|
||||
>
|
||||
<Icon icon='mdi:bookmark' width={12} height={12} />
|
||||
{` ${formatTitleCase(projectFlock.category ?? '')}`}
|
||||
{` ${formatTitleCase(projectFlock.category)}`}
|
||||
</Badge>
|
||||
</div>
|
||||
{/* Information Grid */}
|
||||
@@ -200,7 +168,7 @@ const ProjectFlockDetail = ({
|
||||
}}
|
||||
>
|
||||
<Icon icon='mdi:account-circle' width={14} height={14} />{' '}
|
||||
{projectFlock.created_user?.name}
|
||||
{projectFlock.created_user.name}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -226,7 +194,7 @@ const ProjectFlockDetail = ({
|
||||
>
|
||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Area
|
||||
</div>
|
||||
<div className='col-span-2'>{projectFlock?.area?.name}</div>
|
||||
<div className='col-span-2'>{projectFlock.area.name}</div>
|
||||
|
||||
{/* BARIS 2 */}
|
||||
<div
|
||||
@@ -236,7 +204,7 @@ const ProjectFlockDetail = ({
|
||||
>
|
||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Lokasi
|
||||
</div>
|
||||
<div className='col-span-2'>{projectFlock?.location?.name}</div>
|
||||
<div className='col-span-2'>{projectFlock.location.name}</div>
|
||||
|
||||
<div
|
||||
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
|
||||
@@ -245,7 +213,7 @@ const ProjectFlockDetail = ({
|
||||
>
|
||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> FCR
|
||||
</div>
|
||||
<div className='col-span-2'>{projectFlock?.fcr?.name}</div>
|
||||
<div className='col-span-2'>{projectFlock.fcr.name}</div>
|
||||
|
||||
{/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */}
|
||||
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
|
||||
@@ -253,7 +221,7 @@ const ProjectFlockDetail = ({
|
||||
Kategori
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
{formatTitleCase(projectFlock.category ?? '')}
|
||||
{formatTitleCase(projectFlock.category)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,9 +231,6 @@ const ProjectFlockDetail = ({
|
||||
<div className='border-t-1 border-gray-300'>
|
||||
<div className='p-4 flex flex-col gap-4'>
|
||||
<h2 className='text-2xl font-semibold'>Kandang Aktif</h2>
|
||||
{kandangApprovals && !kandangApprovalsLoading && (
|
||||
<ApprovalSteps approvals={kandangApprovals} />
|
||||
)}
|
||||
{/* Badge Row */}
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Badge
|
||||
@@ -281,7 +246,7 @@ const ProjectFlockDetail = ({
|
||||
height={12}
|
||||
color={'success'}
|
||||
/>{' '}
|
||||
Kandang Aktif ({projectFlock.kandangs?.length})
|
||||
Kandang Aktif ({projectFlock.kandangs.length})
|
||||
</Badge>
|
||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
||||
<Badge
|
||||
@@ -324,7 +289,7 @@ const ProjectFlockDetail = ({
|
||||
<span>Jenis Produk</span>
|
||||
</div>
|
||||
<div className='text-end text-gray-500'>
|
||||
{budget?.nonstock?.name}
|
||||
{budget.nonstock?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row justify-between items-center'>
|
||||
@@ -333,7 +298,7 @@ const ProjectFlockDetail = ({
|
||||
<span>Nama Satuan</span>
|
||||
</div>
|
||||
<div className='text-end text-gray-500'>
|
||||
{budget?.nonstock?.uom?.name}
|
||||
{budget.nonstock?.uom.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row justify-between items-center'>
|
||||
@@ -388,21 +353,21 @@ const ProjectFlockDetail = ({
|
||||
value={selectedKandangId?.toString()}
|
||||
size='md'
|
||||
color='neutral'
|
||||
disabled={projectFlock?.approval?.step_number == 1}
|
||||
disabled={projectFlock.approval.step_number == 1}
|
||||
>
|
||||
{projectFlock.kandangs?.map((kandang) => (
|
||||
{projectFlock.kandangs.map((kandang) => (
|
||||
<div
|
||||
key={kandang.id}
|
||||
className={`grid grid-cols-2 gap-6 cursor-pointer hover:text-gray-800`}
|
||||
onClick={() =>
|
||||
projectFlock?.approval?.step_number > 1 &&
|
||||
setSelectedKamdangId(kandang?.id?.toString())
|
||||
projectFlock.approval.step_number > 1 &&
|
||||
setSelectedKamdangId(kandang.id.toString())
|
||||
}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={kandang?.id?.toString()}
|
||||
label={kandang?.name}
|
||||
disabled={projectFlock?.approval?.step_number == 1}
|
||||
value={kandang.id.toString()}
|
||||
label={kandang.name}
|
||||
disabled={projectFlock.approval.step_number == 1}
|
||||
/>
|
||||
<div className='text-end'>
|
||||
<Badge
|
||||
@@ -410,7 +375,7 @@ const ProjectFlockDetail = ({
|
||||
badge: 'rounded-lg',
|
||||
}}
|
||||
>
|
||||
Kapasitas {kandang?.capacity} Ekor
|
||||
Kapasitas {kandang.capacity} Ekor
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,15 +392,14 @@ const ProjectFlockDetail = ({
|
||||
variant='outline'
|
||||
color='success'
|
||||
disabled={
|
||||
!selectedKandangId ||
|
||||
projectFlock?.approval?.step_number == 1
|
||||
!selectedKandangId || projectFlock.approval.step_number == 1
|
||||
}
|
||||
>
|
||||
Chickin <Icon icon='mdi:checkbox-marked-outline' />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`}
|
||||
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandangId}`}
|
||||
className='m-0 p-0'
|
||||
>
|
||||
<Button
|
||||
@@ -443,8 +407,7 @@ const ProjectFlockDetail = ({
|
||||
variant='outline'
|
||||
color='error'
|
||||
disabled={
|
||||
!selectedKandangId ||
|
||||
projectFlock?.approval?.step_number == 1
|
||||
!selectedKandangId || projectFlock.approval.step_number == 1
|
||||
}
|
||||
>
|
||||
Close <Icon icon='mdi:checkbox-marked-circle-outline' />
|
||||
|
||||
@@ -47,7 +47,9 @@ import Card from '@/components/Card';
|
||||
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
|
||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import Link from 'next/link';
|
||||
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||
import { formatDate } from '@/lib/helper';
|
||||
|
||||
interface ProjectFlockFormProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -258,9 +260,7 @@ const ProjectFlockForm = ({
|
||||
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldValue('category', (val as OptionType)?.value);
|
||||
formik.setFieldValue('category_option', val);
|
||||
if (val == null) {
|
||||
formik.setFieldTouched('category', true);
|
||||
}
|
||||
formik.setFieldTouched('category', true);
|
||||
};
|
||||
|
||||
// Submit Handler
|
||||
@@ -788,7 +788,7 @@ const ProjectFlockForm = ({
|
||||
}
|
||||
errorMessage={formik.errors.area_id as string}
|
||||
isClearable
|
||||
isDisabled={formType != 'add'}
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
<SelectInput
|
||||
required
|
||||
@@ -807,7 +807,7 @@ const ProjectFlockForm = ({
|
||||
}
|
||||
errorMessage={formik.errors.location_id as string}
|
||||
isClearable
|
||||
isDisabled={formType != 'add' || disabledLocation}
|
||||
isDisabled={formType === 'detail' || disabledLocation}
|
||||
/>
|
||||
<SelectInput
|
||||
required
|
||||
@@ -837,7 +837,7 @@ const ProjectFlockForm = ({
|
||||
}
|
||||
errorMessage={formik.errors.flock_name as string}
|
||||
isClearable
|
||||
isDisabled={formType != 'add'}
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
<SelectInput
|
||||
required
|
||||
@@ -851,7 +851,7 @@ const ProjectFlockForm = ({
|
||||
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
|
||||
errorMessage={formik.errors.fcr_id as string}
|
||||
isClearable
|
||||
isDisabled={formType != 'add'}
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
<SelectInput
|
||||
required
|
||||
@@ -864,7 +864,7 @@ const ProjectFlockForm = ({
|
||||
}
|
||||
errorMessage={formik.errors.category as string}
|
||||
isClearable
|
||||
isDisabled={formType != 'add'}
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
<NumberInput
|
||||
name='period'
|
||||
|
||||
@@ -50,17 +50,12 @@ const RowOptionsMenu = ({
|
||||
);
|
||||
};
|
||||
|
||||
const isRecordingRejected = (recording: Recording) => {
|
||||
return recording.approval?.action === 'REJECTED';
|
||||
};
|
||||
|
||||
const isApproved = isRecordingApproved(props.row.original);
|
||||
const isRejected = isRecordingRejected(props.row.original);
|
||||
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<Button
|
||||
href={`/production/recording/detail/?recordingId=${props.row.original.id}`}
|
||||
href={`recording/detail/?recordingId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
@@ -69,7 +64,7 @@ const RowOptionsMenu = ({
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
|
||||
href={`recording/detail/edit/?recordingId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
@@ -77,7 +72,7 @@ const RowOptionsMenu = ({
|
||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
{!isApproved && !isRejected && (
|
||||
{!isApproved && (
|
||||
<Button
|
||||
onClick={approveClickHandler}
|
||||
variant='ghost'
|
||||
@@ -88,7 +83,7 @@ const RowOptionsMenu = ({
|
||||
Approve
|
||||
</Button>
|
||||
)}
|
||||
{!isApproved && !isRejected && (
|
||||
{!isApproved && (
|
||||
<Button
|
||||
onClick={rejectClickHandler}
|
||||
variant='ghost'
|
||||
@@ -515,7 +510,7 @@ const RecordingTable = () => {
|
||||
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
||||
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
||||
<Button
|
||||
href='/production/recording/add'
|
||||
href='recording/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
|
||||
@@ -112,10 +112,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
);
|
||||
}, []);
|
||||
|
||||
const isRecordingRejected = useCallback((recording?: Recording) => {
|
||||
return recording?.approval?.action === 'REJECTED';
|
||||
}, []);
|
||||
|
||||
// ===== PAYLOAD CREATION HELPERS =====
|
||||
const createGrowingPayload = useCallback(
|
||||
(values: RecordingGrowingFormValues) => {
|
||||
@@ -1487,48 +1483,37 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
{type === 'detail' &&
|
||||
initialValues?.approval &&
|
||||
!isRecordingApproved(initialValues) &&
|
||||
!isRecordingRejected(initialValues) && (
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='success'
|
||||
onClick={() => {
|
||||
setApprovalNotes('');
|
||||
approveModal.openModal();
|
||||
}}
|
||||
isLoading={isApproveLoading}
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:check'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
Approve
|
||||
</Button>
|
||||
{type === 'detail' && !isRecordingApproved(initialValues) && (
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='success'
|
||||
onClick={() => {
|
||||
setApprovalNotes('');
|
||||
approveModal.openModal();
|
||||
}}
|
||||
isLoading={isApproveLoading}
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='outline'
|
||||
color='error'
|
||||
onClick={() => {
|
||||
setApprovalNotes('');
|
||||
rejectModal.openModal();
|
||||
}}
|
||||
isLoading={isRejectLoading}
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:close'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant='outline'
|
||||
color='error'
|
||||
onClick={() => {
|
||||
setApprovalNotes('');
|
||||
rejectModal.openModal();
|
||||
}}
|
||||
isLoading={isRejectLoading}
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className='text-2xl font-bold text-center'>
|
||||
@@ -2818,8 +2803,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
|
||||
{/* Approve Confirmation Modal */}
|
||||
{(type as 'add' | 'edit' | 'detail') === 'detail' &&
|
||||
!isRecordingApproved(initialValues) &&
|
||||
!isRecordingRejected(initialValues) && (
|
||||
!isRecordingApproved(initialValues) && (
|
||||
<ConfirmationModalWithNotes
|
||||
ref={approveModal.ref}
|
||||
type='success'
|
||||
|
||||
+3
-1
@@ -226,7 +226,9 @@ export const getFilledTransferToLayingFormInitialValues = async (
|
||||
// targetKandang.target_project_flock_kandang.kandang.capacity,
|
||||
|
||||
// TODO: integrate this to real API kandang capacity
|
||||
maxQuantity: Infinity,
|
||||
maxQuantity:
|
||||
targetKandang.target_project_flock_kandang.kandang.capacity ??
|
||||
Infinity,
|
||||
}))
|
||||
: [],
|
||||
|
||||
|
||||
@@ -1,413 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { pdf } from '@react-pdf/renderer';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/dropdown/Dropdown';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import DailyMarketingsTable from '@/components/pages/report/DailyMarketingsTable';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import DailyMarketingReportPDF from '@/components/pages/report/DailyMarketingReportPDF';
|
||||
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import {
|
||||
AreaApi,
|
||||
CustomerApi,
|
||||
LocationApi,
|
||||
WarehouseApi,
|
||||
} from '@/services/api/master-data';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { MarketingReportApi } from '@/services/api/report/marketing-report';
|
||||
import { MARKETING_TYPE_OPTIONS } from '@/config/constant';
|
||||
import { httpClient } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { DailyMarketingReport } from '@/types/api/report/marketing';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
|
||||
const DailyMarketingReportContent = () => {
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
reset: resetFilter,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
area_id: '',
|
||||
location_id: '',
|
||||
warehouse_id: '',
|
||||
customer_id: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
marketing_type: '',
|
||||
filter_by: '',
|
||||
sort_by: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
area_id: 'area_id',
|
||||
location_id: 'location_id',
|
||||
warehouse_id: 'warehouse_id',
|
||||
customer_id: 'customer_id',
|
||||
start_date: 'start_date',
|
||||
end_date: 'end_date',
|
||||
marketing_type: 'marketing_type',
|
||||
filter_by: 'filter_by',
|
||||
sort_by: 'sort_by',
|
||||
},
|
||||
});
|
||||
|
||||
const dailyMarketingsReportUrl = `${MarketingReportApi.basePath}${getTableFilterQueryString()}`;
|
||||
|
||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||
useState(false);
|
||||
const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false);
|
||||
|
||||
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
||||
const {
|
||||
setInputValue: setAreaInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
|
||||
|
||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedArea(val as OptionType);
|
||||
updateFilter('area_id', val ? ((val as OptionType).value as string) : '');
|
||||
};
|
||||
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||
|
||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedLocation(val as OptionType);
|
||||
updateFilter(
|
||||
'location_id',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
);
|
||||
};
|
||||
|
||||
const [selectedWarehouse, setSelectedWarehouse] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const {
|
||||
setInputValue: setWarehouseInputValue,
|
||||
options: warehouseOptions,
|
||||
isLoadingOptions: isLoadingWarehouseOptions,
|
||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
||||
|
||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedWarehouse(val as OptionType);
|
||||
updateFilter(
|
||||
'warehouse_id',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
);
|
||||
};
|
||||
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const {
|
||||
setInputValue: setCustomerInputValue,
|
||||
options: customerOptions,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedCustomer(val as OptionType);
|
||||
updateFilter(
|
||||
'customer_id',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
);
|
||||
};
|
||||
|
||||
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFilter('start_date', e.target.value ? e.target.value : '');
|
||||
};
|
||||
|
||||
const endDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFilter('end_date', e.target.value ? e.target.value : '');
|
||||
};
|
||||
|
||||
const [selectedMarketingType, setSelectedMarketingType] =
|
||||
useState<OptionType | null>(null);
|
||||
const marketingTypeChangeHandler = (
|
||||
val: OptionType | OptionType[] | null
|
||||
) => {
|
||||
setSelectedMarketingType(val as OptionType);
|
||||
updateFilter(
|
||||
'marketing_type',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
);
|
||||
};
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
const filterByChangeHandler = (filterBy: string) => {
|
||||
updateFilter('filter_by', filterBy);
|
||||
};
|
||||
|
||||
const sortByChangeHandler = (sort: 'asc' | 'desc' | '') => {
|
||||
updateFilter('sort_by', sort);
|
||||
};
|
||||
|
||||
const exportToExcelHandler = async () => {
|
||||
setIsLoadingExportingToExcel(true);
|
||||
|
||||
await MarketingReportApi.exportDailyMarketingToExcel(
|
||||
getTableFilterQueryString()
|
||||
);
|
||||
|
||||
setIsLoadingExportingToExcel(false);
|
||||
};
|
||||
|
||||
const exportToPdfHandler = async () => {
|
||||
setIsLoadingExportingToPdf(true);
|
||||
|
||||
const params = new URLSearchParams(getTableFilterQueryString());
|
||||
|
||||
params.set('limit', '9999999');
|
||||
|
||||
const queryString = `?${params.toString()}`;
|
||||
|
||||
try {
|
||||
const dailyMarketingsReport = await httpClient<
|
||||
BaseApiResponse<DailyMarketingReport>
|
||||
>(`${MarketingReportApi.basePath}${queryString}`);
|
||||
|
||||
if (isResponseError(dailyMarketingsReport)) {
|
||||
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
|
||||
return;
|
||||
}
|
||||
|
||||
const openPdf = async () => {
|
||||
const dailyMarketingReportPdfBlob = await pdf(
|
||||
<DailyMarketingReportPDF data={dailyMarketingsReport.data} />
|
||||
).toBlob();
|
||||
|
||||
const dailyMarketingReportPdfUrl = URL.createObjectURL(
|
||||
dailyMarketingReportPdfBlob
|
||||
);
|
||||
window.open(dailyMarketingReportPdfUrl, '_blank');
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
const blob = await pdf(
|
||||
<DailyMarketingReportPDF data={dailyMarketingsReport.data} />
|
||||
).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'laporan-penjualan-harian.pdf';
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
await openPdf();
|
||||
} catch (error) {
|
||||
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
|
||||
}
|
||||
|
||||
setIsLoadingExportingToPdf(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedArea(null);
|
||||
setSelectedLocation(null);
|
||||
setSelectedWarehouse(null);
|
||||
setSelectedCustomer(null);
|
||||
setSelectedMarketingType(null);
|
||||
resetFilter();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full border border-gray-200 p-4'>
|
||||
<div>
|
||||
<h2 className='text-xl font-bold text-center'>Penjualan Harian</h2>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className='flex flex-col gap-4 mb-6'>
|
||||
<div className='grid grid-cols-12 gap-4'>
|
||||
<SelectInput
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
options={areaOptions}
|
||||
isLoading={isLoadingAreaOptions}
|
||||
value={selectedArea}
|
||||
onChange={areaChangeHandler}
|
||||
onInputChange={setAreaInputValue}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
placeholder='Pilih Lokasi'
|
||||
options={locationOptions}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
value={selectedLocation}
|
||||
onChange={locationChangeHandler}
|
||||
onInputChange={setLocationInputValue}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Gudang'
|
||||
placeholder='Pilih Gudang'
|
||||
options={warehouseOptions}
|
||||
isLoading={isLoadingWarehouseOptions}
|
||||
value={selectedWarehouse}
|
||||
onChange={warehouseChangeHandler}
|
||||
onInputChange={setWarehouseInputValue}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Customer'
|
||||
placeholder='Pilih Customer'
|
||||
options={customerOptions}
|
||||
isLoading={isLoadingCustomerOptions}
|
||||
value={selectedCustomer}
|
||||
onChange={customerChangeHandler}
|
||||
onInputChange={setCustomerInputValue}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
name='startDate'
|
||||
label='Tanggal Awal'
|
||||
placeholder='Tanggal Realisasi'
|
||||
value={tableFilterState.start_date}
|
||||
onChange={startDateChangeHandler}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
name='endDate'
|
||||
label='Tanggal Akhir'
|
||||
placeholder='Tanggal Realisasi'
|
||||
value={tableFilterState.end_date}
|
||||
onChange={endDateChangeHandler}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-12 gap-4'>
|
||||
<SelectInput
|
||||
label='Tipe Marketing'
|
||||
placeholder='Pilih Tipe Marketing'
|
||||
options={MARKETING_TYPE_OPTIONS}
|
||||
value={selectedMarketingType}
|
||||
onChange={marketingTypeChangeHandler}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className='col-span-12 sm:col-span-6 lg:col-span-8 flex flex-wrap sm:justify-end items-end gap-2'>
|
||||
<Button
|
||||
color='primary'
|
||||
// onClick={handleSearch}
|
||||
className='flex-1 sm:flex-none'
|
||||
>
|
||||
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||
Cari
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color='warning'
|
||||
onClick={handleReset}
|
||||
className='flex-1 sm:flex-none'
|
||||
>
|
||||
<Icon icon='heroicons-outline:refresh' width={20} height={20} />
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
align='end'
|
||||
direction='bottom'
|
||||
trigger={
|
||||
<Button>
|
||||
Export{' '}
|
||||
<Icon
|
||||
icon='heroicons-outline:download'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
title='Export to Excel'
|
||||
icon='icon-park-outline:excel'
|
||||
isLoading={isLoadingExportingToExcel}
|
||||
onClick={exportToExcelHandler}
|
||||
className='text-nowrap'
|
||||
/>
|
||||
<MenuItem
|
||||
title='Export to PDF'
|
||||
icon='icon-park-outline:file-pdf-one'
|
||||
onClick={exportToPdfHandler}
|
||||
className='text-nowrap'
|
||||
/>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DailyMarketingsTable
|
||||
dailyMarketingsReportUrl={dailyMarketingsReportUrl}
|
||||
onSetPage={setPage}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
onSetPageSize={setPageSize}
|
||||
searchValue={tableFilterState.search}
|
||||
onSearchChange={searchChangeHandler}
|
||||
onFilterByChange={filterByChangeHandler}
|
||||
onSortByChange={sortByChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyMarketingReportContent;
|
||||
@@ -1,550 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Document,
|
||||
Image,
|
||||
Page,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from '@react-pdf/renderer';
|
||||
|
||||
import { DailyMarketingReport } from '@/types/api/report/marketing';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
|
||||
interface DailyMarketingReportPDFProps {
|
||||
data?: DailyMarketingReport;
|
||||
}
|
||||
|
||||
const DailyMarketingReportPDFStyle = StyleSheet.create({
|
||||
page: {
|
||||
paddingTop: 24,
|
||||
paddingBottom: 64,
|
||||
paddingHorizontal: 16, // Reduce padding to fit more columns
|
||||
orientation: 'landscape',
|
||||
},
|
||||
|
||||
companyInfoHeader: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyLogo: {
|
||||
width: 64,
|
||||
height: 'auto',
|
||||
},
|
||||
companyInfoHeaderDate: {
|
||||
paddingTop: 8,
|
||||
fontSize: 10,
|
||||
},
|
||||
companyName: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
companyAddress: {
|
||||
fontSize: 8,
|
||||
maxWidth: 400,
|
||||
marginBottom: 10,
|
||||
},
|
||||
|
||||
title: {
|
||||
marginTop: 16,
|
||||
fontSize: 14,
|
||||
lineHeight: '150%',
|
||||
textAlign: 'center',
|
||||
fontFamily: 'Times-Roman',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
footer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
|
||||
position: 'absolute',
|
||||
fontSize: 8,
|
||||
bottom: 30,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
color: 'grey',
|
||||
},
|
||||
|
||||
// Table Styles
|
||||
table: {
|
||||
width: '100%',
|
||||
marginTop: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
borderBottomWidth: 0,
|
||||
fontSize: 7, // Smaller font for report
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
alignItems: 'center',
|
||||
minHeight: 20,
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#f0f0f0',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
// Columns definition (Total 100%)
|
||||
colNo: {
|
||||
width: '3%',
|
||||
padding: 2,
|
||||
textAlign: 'center',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colSoDate: {
|
||||
width: '6%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colDoDate: {
|
||||
width: '6%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colAging: {
|
||||
width: '3%',
|
||||
padding: 2,
|
||||
textAlign: 'center',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colWarehouse: {
|
||||
width: '7%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colCustomer: {
|
||||
width: '9%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
}, // Reduced slightly
|
||||
colSales: {
|
||||
width: '6%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colProduct: {
|
||||
width: '8%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
}, // Reduced slightly
|
||||
colDoNumber: {
|
||||
width: '7%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colVehicle: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colMarketingType: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colQty: {
|
||||
width: '4%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colAvgWeight: {
|
||||
width: '4%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colTotalWeight: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colSalesPrice: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colHppPrice: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colSalesAmount: {
|
||||
width: '6%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colHppAmount: { width: '6%', padding: 2, textAlign: 'right' }, // Last column
|
||||
|
||||
// Text inside columns
|
||||
cellText: {
|
||||
fontSize: 6,
|
||||
},
|
||||
headerText: {
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
// Utils
|
||||
doubleDivider: {
|
||||
width: '100%',
|
||||
height: 6,
|
||||
borderTop: '2px solid black',
|
||||
borderBottom: '2px solid black',
|
||||
},
|
||||
|
||||
// Summary
|
||||
summaryContainer: {
|
||||
marginTop: 12,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
width: '100%',
|
||||
},
|
||||
summaryTable: {
|
||||
width: '30%',
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
fontSize: 8,
|
||||
},
|
||||
summaryRow: {
|
||||
flexDirection: 'row',
|
||||
padding: 2,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eee',
|
||||
},
|
||||
summaryLabel: {
|
||||
width: '50%',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
summaryValue: {
|
||||
width: '50%',
|
||||
textAlign: 'right',
|
||||
},
|
||||
});
|
||||
|
||||
const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => {
|
||||
const rows = data?.rows || [];
|
||||
const summary = data?.summary;
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page
|
||||
style={DailyMarketingReportPDFStyle.page}
|
||||
orientation='landscape'
|
||||
size='A4'
|
||||
>
|
||||
<View>
|
||||
<View style={DailyMarketingReportPDFStyle.companyInfoHeader}>
|
||||
<Image
|
||||
style={DailyMarketingReportPDFStyle.companyLogo}
|
||||
src='/assets/img/lti-logo.png'
|
||||
/>
|
||||
|
||||
<Text style={DailyMarketingReportPDFStyle.companyInfoHeaderDate}>
|
||||
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={DailyMarketingReportPDFStyle.companyName}>
|
||||
PT LUMBUNG TELUR INDONESIA
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.companyAddress}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
|
||||
<View style={DailyMarketingReportPDFStyle.doubleDivider} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={DailyMarketingReportPDFStyle.title}>
|
||||
Laporan Penjualan Harian
|
||||
</Text>
|
||||
|
||||
{/* Data Table */}
|
||||
<View style={DailyMarketingReportPDFStyle.table}>
|
||||
{/* Header */}
|
||||
<View
|
||||
style={[
|
||||
DailyMarketingReportPDFStyle.tableRow,
|
||||
DailyMarketingReportPDFStyle.tableHeader,
|
||||
]}
|
||||
>
|
||||
<View style={DailyMarketingReportPDFStyle.colNo}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>No</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSoDate}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Tgl SO
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colDoDate}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Tgl DO
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colAging}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Aging</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Gudang
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colCustomer}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Pelanggan
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSales}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Sales</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colProduct}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Produk
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>No DO</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colVehicle}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Plat No
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Tipe</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colQty}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Qty</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Rerata
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Berat</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Hrg Jual
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
HPP/kg
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Total Jual
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Total HPP
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Rows */}
|
||||
{rows.map((row, index) => (
|
||||
<View style={DailyMarketingReportPDFStyle.tableRow} key={index}>
|
||||
<View style={DailyMarketingReportPDFStyle.colNo}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{index + 1}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSoDate}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatDate(row.so_date, 'DD/MM/YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colDoDate}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatDate(row.do_date, 'DD/MM/YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colAging}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.aging_days}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.warehouse?.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colCustomer}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.customer?.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSales}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.sales}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colProduct}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.product?.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.do_number}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colVehicle}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.vehicle_number}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.marketing_type}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colQty}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatNumber(row.qty)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatNumber(row.average_weight_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatNumber(row.total_weight_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatCurrency(row.sales_price_per_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatCurrency(row.hpp_price_per_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatCurrency(row.sales_amount)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatCurrency(row.hpp_amount)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Summary */}
|
||||
<View style={DailyMarketingReportPDFStyle.summaryContainer}>
|
||||
<View style={DailyMarketingReportPDFStyle.summaryTable}>
|
||||
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||
Total Qty:
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||
{formatNumber(summary?.total_qty ?? 0)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||
Total Berat (kg):
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||
{formatNumber(summary?.total_weight_kg ?? 0)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||
Total Penjualan:
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||
{formatCurrency(summary?.total_sales_amount ?? 0)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
DailyMarketingReportPDFStyle.summaryRow,
|
||||
{ borderBottomWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||
Total HPP:
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||
{formatCurrency(summary?.total_hpp_amount ?? 0)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={DailyMarketingReportPDFStyle.footer} fixed>
|
||||
<Text
|
||||
render={({ pageNumber, totalPages }) =>
|
||||
`${pageNumber} / ${totalPages}`
|
||||
}
|
||||
fixed
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyMarketingReportPDF;
|
||||
@@ -1,255 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Card from '@/components/Card';
|
||||
import Collapse from '@/components/Collapse';
|
||||
|
||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { DailyMarketingRow } from '@/types/api/report/marketing';
|
||||
import { MarketingReportApi } from '@/services/api/report/marketing-report';
|
||||
|
||||
interface DailyMarketingsTableProps {
|
||||
dailyMarketingsReportUrl: string;
|
||||
onSetPage: (page: number) => void;
|
||||
pageSize: number;
|
||||
onSetPageSize: (pageSize: number) => void;
|
||||
searchValue: string;
|
||||
onSearchChange: ChangeEventHandler<HTMLInputElement>;
|
||||
onFilterByChange: (filterBy: string) => void;
|
||||
onSortByChange: (sort: 'asc' | 'desc' | '') => void;
|
||||
}
|
||||
|
||||
const DailyMarketingsTable = ({
|
||||
dailyMarketingsReportUrl,
|
||||
onSetPage,
|
||||
pageSize,
|
||||
onSetPageSize,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onFilterByChange,
|
||||
onSortByChange,
|
||||
}: DailyMarketingsTableProps) => {
|
||||
const { data: dailyMarketings, isLoading: isLoadingDailyMarketings } = useSWR(
|
||||
dailyMarketingsReportUrl,
|
||||
MarketingReportApi.getAllDailyMarketingFetcher,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const dailyMarketingColumns: ColumnDef<DailyMarketingRow>[] = [
|
||||
{
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_date',
|
||||
header: 'Tanggal Jual',
|
||||
cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'),
|
||||
footer: 'Total',
|
||||
},
|
||||
{
|
||||
accessorKey: 'do_date',
|
||||
header: 'Tanggal DO',
|
||||
cell: (props) => formatDate(props.row.original.do_date, 'DD-MMM-YYYY'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'aging_days',
|
||||
header: 'Aging',
|
||||
cell: (props) => `${props.row.original.aging_days} hari`,
|
||||
},
|
||||
{
|
||||
accessorKey: 'warehouse.name',
|
||||
header: 'Gudang',
|
||||
},
|
||||
{
|
||||
accessorKey: 'customer.name',
|
||||
header: 'Pelanggan',
|
||||
},
|
||||
{
|
||||
accessorKey: 'do_number',
|
||||
header: 'No. DO',
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales',
|
||||
header: 'Sales/Marketing',
|
||||
},
|
||||
{
|
||||
accessorKey: 'vehicle_number',
|
||||
header: 'No. Polisi',
|
||||
cell: (props) => (
|
||||
<span className='text-nowrap'>{props.row.original.vehicle_number}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'marketing_type',
|
||||
header: 'Marketing Type',
|
||||
},
|
||||
{
|
||||
accessorKey: 'product.name',
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorKey: 'qty',
|
||||
header: 'Kuantitas',
|
||||
cell: (props) => formatNumber(props.row.original.qty),
|
||||
footer: () => {
|
||||
const totalQty = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings.data.summary.total_qty
|
||||
: 0;
|
||||
|
||||
return formatNumber(totalQty);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'average_weight_kg',
|
||||
header: 'Bobot Rata-Rata (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
||||
},
|
||||
{
|
||||
accessorKey: 'total_weight_kg',
|
||||
header: 'Bobot Total (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
||||
footer: () => {
|
||||
const totalWeightKg = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings.data.summary.total_weight_kg
|
||||
: 0;
|
||||
|
||||
return formatNumber(totalWeightKg);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_price_per_kg',
|
||||
header: 'Harga Jual (Rp)',
|
||||
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
||||
},
|
||||
{
|
||||
accessorKey: 'hpp_price_per_kg',
|
||||
header: 'HPP (Rp)',
|
||||
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_amount',
|
||||
header: 'Total (Rp)',
|
||||
cell: (props) => formatCurrency(props.row.original.sales_amount),
|
||||
footer: () => {
|
||||
const totalSalesAmount = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings.data.summary.total_sales_amount
|
||||
: 0;
|
||||
|
||||
return formatCurrency(totalSalesAmount);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (sorting.length === 1) {
|
||||
onFilterByChange(sorting[0].id);
|
||||
onSortByChange(sorting[0].desc ? 'desc' : 'asc');
|
||||
} else {
|
||||
onFilterByChange('');
|
||||
onSortByChange('');
|
||||
}
|
||||
}, [sorting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setOpen(
|
||||
isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings.data.rows.length > 0
|
||||
: false
|
||||
);
|
||||
}
|
||||
}, [dailyMarketings, isResponseSuccess]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Collapse
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={
|
||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||
<div className='card-title'>Penjualan Harian</div>
|
||||
|
||||
<Icon
|
||||
icon='material-symbols:keyboard-arrow-down'
|
||||
width={24}
|
||||
height={24}
|
||||
className={cn('text-primary transition-transform', {
|
||||
'-rotate-180': open,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
className='w-full!'
|
||||
titleClassName='w-full p-0!'
|
||||
>
|
||||
<div className='w-full p-0'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Penjualan Harian'
|
||||
value={searchValue}
|
||||
onChange={onSearchChange}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table<DailyMarketingRow>
|
||||
data={
|
||||
isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.data.rows
|
||||
: []
|
||||
}
|
||||
columns={dailyMarketingColumns}
|
||||
pageSize={pageSize}
|
||||
onPageSizeChange={onSetPageSize}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
page={
|
||||
isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.meta?.page
|
||||
: 0
|
||||
}
|
||||
totalItems={
|
||||
isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.meta?.total_results
|
||||
: 0
|
||||
}
|
||||
onPageChange={onSetPage}
|
||||
isLoading={isLoadingDailyMarketings}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
renderFooter={true}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(dailyMarketings) &&
|
||||
dailyMarketings?.data?.rows.length === 0,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyMarketingsTable;
|
||||
@@ -1,50 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { JSX, useState } from 'react';
|
||||
|
||||
import Tabs from '@/components/Tabs';
|
||||
import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent';
|
||||
import HppPerKandangTab from './sale/tab/HppPerKandangTab';
|
||||
|
||||
type MarketingReportTabType =
|
||||
| 'daily'
|
||||
| 'transaction'
|
||||
| 'hpp-comparison'
|
||||
| 'daily-hpp';
|
||||
|
||||
const marketingReportTabs: {
|
||||
id: MarketingReportTabType;
|
||||
label: string;
|
||||
content: JSX.Element;
|
||||
}[] = [
|
||||
{
|
||||
id: 'daily',
|
||||
label: 'Penjualan Harian',
|
||||
content: <DailyMarketingReportContent />,
|
||||
},
|
||||
{
|
||||
id: 'daily-hpp',
|
||||
label: 'HPP Harian Kandang',
|
||||
content: <HppPerKandangTab />,
|
||||
},
|
||||
];
|
||||
|
||||
const MarketingReportContent = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('daily');
|
||||
|
||||
return (
|
||||
<section className='w-full max-w-7xl pb-16'>
|
||||
<Tabs
|
||||
activeTabId={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
tabs={marketingReportTabs}
|
||||
variant='lifted'
|
||||
className={{
|
||||
content: '-m-px pl-px',
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingReportContent;
|
||||
@@ -1,867 +0,0 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { ChangeEventHandler } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
||||
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { ReportExpenseApi } from '@/services/api/report';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import Pagination from '@/components/Pagination';
|
||||
import Dropdown from '@/components/dropdown/Dropdown';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { generateReportExpensePDF } from './pdf/ReportExpenseExport';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ReportExpenseTable = () => {
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [pdfProgress, setPdfProgress] = useState(0);
|
||||
const [excelProgress, setExcelProgress] = useState(0);
|
||||
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||
|
||||
// ===== SUBMISSION STATE =====
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// ===== TABLE FILTER STATE =====
|
||||
const {
|
||||
state: filterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
reset: resetFilterState,
|
||||
toQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
location_id: '',
|
||||
supplier_id: '',
|
||||
kandang_id: '',
|
||||
nonstock_id: '',
|
||||
realization_date: '',
|
||||
category: '',
|
||||
search: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== SELECT OPTIONS =====
|
||||
const { options: optionsLocation, isLoadingOptions: isLoadingLocation } =
|
||||
useSelect(`/master-data/locations`, 'id', 'name');
|
||||
const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } =
|
||||
useSelect(`/master-data/suppliers`, 'id', 'name');
|
||||
const { options: optionsKandang, isLoadingOptions: isLoadingKandang } =
|
||||
useSelect(`/master-data/kandangs`, 'id', 'name', '', {
|
||||
location_id: filterState.location_id,
|
||||
});
|
||||
const { options: optionsNonstock, isLoadingOptions: isLoadingNonstock } =
|
||||
useSelect(`/master-data/nonstocks`, 'id', 'name');
|
||||
|
||||
const categoryOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'BOP', label: 'BOP' },
|
||||
{ value: 'NON-BOP', label: 'Non BOP' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Mendapatkan value option select dari filter state
|
||||
const selectedLocation = useMemo(
|
||||
() =>
|
||||
optionsLocation.find(
|
||||
(opt) => String(opt.value) === filterState.location_id
|
||||
) || null,
|
||||
[optionsLocation, filterState.location_id]
|
||||
);
|
||||
const selectedSupplier = useMemo(
|
||||
() =>
|
||||
optionsSupplier.find(
|
||||
(opt) => String(opt.value) === filterState.supplier_id
|
||||
) || null,
|
||||
[optionsSupplier, filterState.supplier_id]
|
||||
);
|
||||
const selectedKandang = useMemo(
|
||||
() =>
|
||||
optionsKandang.find(
|
||||
(opt) => String(opt.value) === filterState.kandang_id
|
||||
) || null,
|
||||
[optionsKandang, filterState.kandang_id]
|
||||
);
|
||||
const selectedNonstock = useMemo(
|
||||
() =>
|
||||
optionsNonstock.find(
|
||||
(opt) => String(opt.value) === filterState.nonstock_id
|
||||
) || null,
|
||||
[optionsNonstock, filterState.nonstock_id]
|
||||
);
|
||||
const selectedCategory = useMemo(
|
||||
() =>
|
||||
categoryOptions.find((opt) => opt.value === filterState.category) || null,
|
||||
[categoryOptions, filterState.category]
|
||||
);
|
||||
|
||||
// ===== FILTER CHANGE HANDLERS =====
|
||||
const locationChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const option = val as OptionType;
|
||||
updateFilter('location_id', option ? String(option.value) : '');
|
||||
updateFilter('kandang_id', '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const kandangChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const option = val as OptionType;
|
||||
updateFilter('kandang_id', option ? String(option.value) : '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const supplierChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const option = val as OptionType;
|
||||
updateFilter('supplier_id', option ? String(option.value) : '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const nonstockChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const option = val as OptionType;
|
||||
updateFilter('nonstock_id', option ? String(option.value) : '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const categoryChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const option = val as OptionType;
|
||||
updateFilter('category', option ? String(option.value) : '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const realizationDateChangeHandler = useCallback<
|
||||
ChangeEventHandler<HTMLInputElement>
|
||||
>(
|
||||
(e) => {
|
||||
updateFilter('realization_date', e.target.value || '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const searchChangeHandler = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFilter('search', e.target.value);
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
// ===== RESET FILTERS =====
|
||||
const resetFilters = useCallback(() => {
|
||||
resetFilterState();
|
||||
setIsSubmitted(false);
|
||||
}, [resetFilterState]);
|
||||
|
||||
// ===== SUBMIT HANDLER =====
|
||||
const handleSubmit = useCallback(() => {
|
||||
setIsSubmitted(true);
|
||||
setPage(1);
|
||||
}, [setPage]);
|
||||
|
||||
// ===== DATA FETCHING FOR TABLE =====
|
||||
const { data: reportExpenseResponse, isLoading } = useSWR(
|
||||
isSubmitted
|
||||
? () => {
|
||||
return ['report-expense', toQueryString()];
|
||||
}
|
||||
: null,
|
||||
([, query]) => {
|
||||
const endpoint = `${ReportExpenseApi.basePath}${query}`;
|
||||
return ReportExpenseApi.getAllFetcher(endpoint);
|
||||
}
|
||||
);
|
||||
|
||||
const data: ReportExpense[] = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(reportExpenseResponse)
|
||||
? (reportExpenseResponse?.data as ReportExpense[]) || []
|
||||
: [],
|
||||
[reportExpenseResponse]
|
||||
);
|
||||
|
||||
const meta = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(reportExpenseResponse) && reportExpenseResponse.meta
|
||||
? reportExpenseResponse.meta
|
||||
: null,
|
||||
[reportExpenseResponse]
|
||||
);
|
||||
|
||||
// ===== EXPORT DATA FETCHER =====
|
||||
const reportExpenseExport = useCallback(async (): Promise<
|
||||
ReportExpense[] | null
|
||||
> => {
|
||||
const params = new URLSearchParams(toQueryString().replace('?', ''));
|
||||
params.set('limit', 'limit');
|
||||
params.set('page', '1');
|
||||
|
||||
const endpoint = `${ReportExpenseApi.basePath}?${params.toString()}`;
|
||||
const response = await ReportExpenseApi.getAllFetcher(endpoint);
|
||||
|
||||
return isResponseSuccess(response) ? response.data : null;
|
||||
}, [toQueryString]);
|
||||
|
||||
// ===== EXPORT HANDLERS =====
|
||||
const handleExportPdf = useCallback(async () => {
|
||||
if (isPdfExportLoading) return;
|
||||
setIsPdfExportLoading(true);
|
||||
setPdfProgress(0);
|
||||
|
||||
await new Promise((resolve) =>
|
||||
requestAnimationFrame(() => resolve(undefined))
|
||||
);
|
||||
|
||||
try {
|
||||
// Stage 1: Fetching data (0-20%)
|
||||
setPdfProgress(10);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const allData = await reportExpenseExport();
|
||||
if (!allData || allData.length === 0) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
setIsPdfExportLoading(false);
|
||||
setPdfProgress(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stage 2: Data fetched - langsung loncat ke progress tinggi
|
||||
setPdfProgress(30);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
const progressInterval = setInterval(() => {
|
||||
setPdfProgress((prev) => {
|
||||
// Increment kecil dan random antara 0.5-2%
|
||||
const increment = Math.random() * 1.5 + 0.5;
|
||||
const newProgress = Math.min(prev + increment, 50);
|
||||
return newProgress;
|
||||
});
|
||||
}, 300); // Update setiap 300ms
|
||||
|
||||
const pdfParams = {
|
||||
location_name: selectedLocation?.label,
|
||||
supplier_name: selectedSupplier?.label,
|
||||
kandang_name: selectedKandang?.label,
|
||||
nonstock_name: selectedNonstock?.label,
|
||||
category: selectedCategory?.label,
|
||||
realization_date: filterState.realization_date,
|
||||
search: filterState.search,
|
||||
};
|
||||
|
||||
setDropdownOpen(false);
|
||||
|
||||
// Stage 3: Langsung loncat ke 80-85% untuk menghindari stuck
|
||||
const baseProgress = 80 + Math.floor(Math.random() * 16); // Random 80-85%
|
||||
setPdfProgress(baseProgress);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Stage 4: Berikan jeda untuk UI update
|
||||
await new Promise((resolve) =>
|
||||
requestAnimationFrame(() => resolve(undefined))
|
||||
);
|
||||
|
||||
// Proses PDF yang sebenarnya
|
||||
await generateReportExpensePDF(allData, pdfParams);
|
||||
|
||||
clearInterval(progressInterval);
|
||||
|
||||
// Stage 5: Finalizing (98-100%)
|
||||
setPdfProgress(99);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
setPdfProgress(100);
|
||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||
|
||||
// Reset progress setelah selesai
|
||||
setTimeout(() => setPdfProgress(0), 500);
|
||||
} catch (error) {
|
||||
console.error('PDF Export Error:', error);
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
setPdfProgress(0);
|
||||
} finally {
|
||||
setIsPdfExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
reportExpenseExport,
|
||||
selectedLocation,
|
||||
selectedSupplier,
|
||||
selectedKandang,
|
||||
selectedNonstock,
|
||||
selectedCategory,
|
||||
filterState.realization_date,
|
||||
filterState.search,
|
||||
]);
|
||||
|
||||
const handleExportExcel = useCallback(async () => {
|
||||
if (isExcelExportLoading) return;
|
||||
setIsExcelExportLoading(true);
|
||||
setExcelProgress(0);
|
||||
setDropdownOpen(false);
|
||||
|
||||
await new Promise((resolve) =>
|
||||
requestAnimationFrame(() => resolve(undefined))
|
||||
);
|
||||
|
||||
try {
|
||||
// Stage 1: Fetching data (0-20%)
|
||||
setExcelProgress(15);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const allDataForExport = await reportExpenseExport();
|
||||
|
||||
if (!allDataForExport || allDataForExport.length === 0) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
setIsExcelExportLoading(false);
|
||||
setExcelProgress(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stage 2: Data fetched (20-40%)
|
||||
setExcelProgress(30);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Stage 3: Grouping data (40-60%)
|
||||
setExcelProgress(50);
|
||||
const groupedBySupplier: Record<string, ReportExpense[]> = {};
|
||||
allDataForExport.forEach((item) => {
|
||||
const supplierName = item.supplier?.name || 'Unknown Supplier';
|
||||
if (!groupedBySupplier[supplierName]) {
|
||||
groupedBySupplier[supplierName] = [];
|
||||
}
|
||||
groupedBySupplier[supplierName].push(item);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Stage 4: Creating workbook (60-80%)
|
||||
setExcelProgress(70);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
const supplierEntries = Object.entries(groupedBySupplier);
|
||||
const totalSuppliers = supplierEntries.length;
|
||||
|
||||
for (let i = 0; i < supplierEntries.length; i++) {
|
||||
const [supplierName, supplierData] = supplierEntries[i];
|
||||
|
||||
// Update progress per supplier
|
||||
const progressIncrement = (20 / totalSuppliers) * (i + 1);
|
||||
setExcelProgress(70 + progressIncrement);
|
||||
|
||||
const totals = supplierData.reduce(
|
||||
(acc, item) => ({
|
||||
qty_pengajuan: acc.qty_pengajuan + (item.pengajuan?.qty || 0),
|
||||
total_pengajuan:
|
||||
acc.total_pengajuan +
|
||||
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0),
|
||||
qty_realisasi: acc.qty_realisasi + (item.realisasi?.qty || 0),
|
||||
total_realisasi:
|
||||
acc.total_realisasi +
|
||||
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0),
|
||||
}),
|
||||
{
|
||||
qty_pengajuan: 0,
|
||||
total_pengajuan: 0,
|
||||
qty_realisasi: 0,
|
||||
total_realisasi: 0,
|
||||
}
|
||||
);
|
||||
|
||||
const excelData = supplierData.map((item, index) => ({
|
||||
No: index + 1,
|
||||
'No. PO': item.po_number || '',
|
||||
'No. Referensi': item.reference_number || '',
|
||||
'Tanggal Realisasi': item.realization_date
|
||||
? formatDate(item.realization_date, 'DD MMM YYYY')
|
||||
: '',
|
||||
'Tanggal Transaksi': item.transaction_date
|
||||
? formatDate(item.transaction_date, 'DD MMM YYYY')
|
||||
: '',
|
||||
Kategori: item.category || '',
|
||||
Produk: item.pengajuan?.nonstock?.name || '',
|
||||
Lokasi: item.kandang?.location?.name || '',
|
||||
Kandang: item.kandang?.name || '',
|
||||
'Qty Pengajuan': item.pengajuan?.qty || 0,
|
||||
'Harga Pengajuan': item.pengajuan?.price || 0,
|
||||
'Total Pengajuan':
|
||||
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0),
|
||||
'Qty Realisasi': item.realisasi?.qty || 0,
|
||||
'Harga Realisasi': item.realisasi?.price || 0,
|
||||
'Total Realisasi':
|
||||
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0),
|
||||
'Status Pencairan': item.latest_approval?.step_name || '',
|
||||
}));
|
||||
|
||||
excelData.push({
|
||||
No: 'Total' as unknown as number,
|
||||
'No. PO': '',
|
||||
'No. Referensi': '',
|
||||
'Tanggal Realisasi': '',
|
||||
'Tanggal Transaksi': '',
|
||||
Kategori: '',
|
||||
Produk: '',
|
||||
Lokasi: '',
|
||||
Kandang: '',
|
||||
'Qty Pengajuan': totals.qty_pengajuan,
|
||||
'Harga Pengajuan': 0,
|
||||
'Total Pengajuan': totals.total_pengajuan,
|
||||
'Qty Realisasi': totals.qty_realisasi,
|
||||
'Harga Realisasi': 0,
|
||||
'Total Realisasi': totals.total_realisasi,
|
||||
'Status Pencairan': '',
|
||||
});
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
||||
const colWidths = [
|
||||
{ wch: 5 }, // No
|
||||
{ wch: 20 }, // No. PO
|
||||
{ wch: 20 }, // No. Referensi
|
||||
{ wch: 15 }, // Tanggal Realisasi
|
||||
{ wch: 15 }, // Tanggal Transaksi
|
||||
{ wch: 15 }, // Kategori
|
||||
{ wch: 30 }, // Produk
|
||||
{ wch: 20 }, // Lokasi
|
||||
{ wch: 15 }, // Kandang
|
||||
{ wch: 15 }, // Qty Pengajuan
|
||||
{ wch: 15 }, // Harga Pengajuan
|
||||
{ wch: 20 }, // Total Pengajuan
|
||||
{ wch: 15 }, // Qty Realisasi
|
||||
{ wch: 15 }, // Harga Realisasi
|
||||
{ wch: 20 }, // Total Realisasi
|
||||
{ wch: 20 }, // Status Pencairan
|
||||
];
|
||||
worksheet['!cols'] = colWidths;
|
||||
|
||||
const sheetName = supplierName.slice(0, 31);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||
|
||||
// Small delay to allow UI update
|
||||
if (i < supplierEntries.length - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 5: Writing file (90-100%)
|
||||
setExcelProgress(95);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const filename = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`;
|
||||
XLSX.writeFile(workbook, filename);
|
||||
|
||||
setExcelProgress(100);
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
|
||||
// Reset progress
|
||||
setTimeout(() => setExcelProgress(0), 500);
|
||||
} catch (error) {
|
||||
console.error('Excel Export Error:', error);
|
||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||
setExcelProgress(0);
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [isExcelExportLoading, reportExpenseExport]);
|
||||
|
||||
// ===== PAGINATION HANDLERS =====
|
||||
const handlePageChange = (page: number) => {
|
||||
setPage(page);
|
||||
};
|
||||
|
||||
const handleRowChange = (pageSize: number) => {
|
||||
setPageSize(pageSize);
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (meta && filterState.page < meta.total_pages) {
|
||||
setPage(filterState.page + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if (filterState.page > 1) {
|
||||
setPage(filterState.page - 1);
|
||||
}
|
||||
};
|
||||
|
||||
// ===== TABLE COLUMNS DEFINITION =====
|
||||
const columns = useMemo((): ColumnDef<ReportExpense>[] => {
|
||||
return [
|
||||
{
|
||||
header: 'No',
|
||||
accessorFn: (_, index) =>
|
||||
(filterState.page - 1) * filterState.pageSize + index + 1,
|
||||
},
|
||||
{
|
||||
header: 'No. PO',
|
||||
accessorKey: 'po_number',
|
||||
},
|
||||
{
|
||||
header: 'No. Referensi',
|
||||
accessorKey: 'reference_number',
|
||||
},
|
||||
{
|
||||
header: 'Tanggal Realisasi',
|
||||
accessorKey: 'realization_date',
|
||||
cell: ({ row }) => {
|
||||
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Tanggal Transaksi',
|
||||
accessorKey: 'transaction_date',
|
||||
cell: ({ row }) => {
|
||||
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kategori',
|
||||
accessorKey: 'category',
|
||||
},
|
||||
{
|
||||
header: 'Produk',
|
||||
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
||||
},
|
||||
{
|
||||
header: 'Supplier',
|
||||
accessorFn: (row) => row.supplier?.name,
|
||||
},
|
||||
{
|
||||
header: 'Lokasi',
|
||||
accessorFn: (row) => row.kandang?.location?.name,
|
||||
},
|
||||
{
|
||||
header: 'Kandang',
|
||||
accessorFn: (row) => row.kandang?.name,
|
||||
},
|
||||
{
|
||||
header: 'Pengajuan',
|
||||
columns: [
|
||||
{
|
||||
header: 'Qty',
|
||||
id: 'qty_pengajuan',
|
||||
accessorFn: (row) => row.pengajuan?.qty,
|
||||
cell: ({ row }) =>
|
||||
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
||||
},
|
||||
{
|
||||
header: 'Harga',
|
||||
id: 'harga_pengajuan',
|
||||
accessorFn: (row) => row.pengajuan?.price,
|
||||
cell: ({ row }) =>
|
||||
formatCurrency(row.original.pengajuan?.price || 0),
|
||||
},
|
||||
{
|
||||
header: 'Total',
|
||||
id: 'total_pengajuan',
|
||||
accessorFn: (row) =>
|
||||
(row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0),
|
||||
cell: ({ row }) => {
|
||||
const total =
|
||||
(row.original.pengajuan?.qty || 0) *
|
||||
(row.original.pengajuan?.price || 0);
|
||||
return formatCurrency(total);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Realisasi',
|
||||
columns: [
|
||||
{
|
||||
header: 'Qty',
|
||||
id: 'qty_realisasi',
|
||||
accessorFn: (row) => row.realisasi?.qty,
|
||||
cell: ({ row }) =>
|
||||
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
||||
},
|
||||
{
|
||||
header: 'Harga',
|
||||
id: 'harga_realisasi',
|
||||
accessorFn: (row) => row.realisasi?.price,
|
||||
cell: ({ row }) =>
|
||||
formatCurrency(row.original.realisasi?.price || 0),
|
||||
},
|
||||
{
|
||||
header: 'Total',
|
||||
id: 'total_realisasi',
|
||||
accessorFn: (row) =>
|
||||
(row.realisasi?.qty || 0) * (row.realisasi?.price || 0),
|
||||
cell: ({ row }) => {
|
||||
const total =
|
||||
(row.original.realisasi?.qty || 0) *
|
||||
(row.original.realisasi?.price || 0);
|
||||
return formatCurrency(total);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Status Pencairan',
|
||||
cell: (props) => (
|
||||
<RealizationStatusBadge
|
||||
approval={props.row.original?.latest_approval}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Status BOP',
|
||||
cell: (props) => (
|
||||
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [filterState.page, filterState.pageSize]);
|
||||
|
||||
// ===== RENDER =====
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{isAnyExportLoading && (
|
||||
<div className='flex flex-col gap-2'>
|
||||
<progress
|
||||
className='progress progress-primary w-full'
|
||||
value={
|
||||
isPdfExportLoading
|
||||
? pdfProgress
|
||||
: isExcelExportLoading
|
||||
? excelProgress
|
||||
: 0
|
||||
}
|
||||
max='100'
|
||||
></progress>
|
||||
{((isPdfExportLoading && pdfProgress > 0) ||
|
||||
(isExcelExportLoading && excelProgress > 0)) && (
|
||||
<div className='text-sm text-center text-gray-600'>
|
||||
<div className='font-semibold'>
|
||||
{(() => {
|
||||
const currentProgress = isPdfExportLoading
|
||||
? pdfProgress
|
||||
: excelProgress;
|
||||
const exportType = isPdfExportLoading ? 'PDF' : 'Excel';
|
||||
|
||||
if (currentProgress < 20)
|
||||
return 'Mengambil data dari server...';
|
||||
if (currentProgress < 30) return 'Memproses data laporan...';
|
||||
if (currentProgress < 40)
|
||||
return `Menyiapkan struktur dokumen ${exportType}...`;
|
||||
if (currentProgress < 50)
|
||||
return 'Mengelompokkan data per supplier...';
|
||||
if (currentProgress < 70)
|
||||
return 'Merender tabel dan kalkulasi...';
|
||||
if (currentProgress < 96)
|
||||
return `Memformat dokumen ${exportType}...`;
|
||||
if (currentProgress < 100)
|
||||
return 'Menyelesaikan dan mengunduh...';
|
||||
return 'Selesai!';
|
||||
})()}{' '}
|
||||
{Math.round(isPdfExportLoading ? pdfProgress : excelProgress)}%
|
||||
</div>
|
||||
{((isPdfExportLoading && pdfProgress >= 35 && pdfProgress < 90) ||
|
||||
(isExcelExportLoading &&
|
||||
excelProgress >= 35 &&
|
||||
excelProgress < 90)) && (
|
||||
<div className='text-xs text-gray-500 mt-1'>
|
||||
{(isPdfExportLoading ? pdfProgress : excelProgress) < 96
|
||||
? 'Proses ini membutuhkan waktu lebih lama untuk data dalam jumlah besar. Mohon bersabar...'
|
||||
: 'Sedang memproses baris data. Hampir selesai...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Card
|
||||
title='Laporan Biaya Operasional'
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
footer={
|
||||
<div className='flex flex-col gap-6'>
|
||||
<div className='flex flex-row items-center justify-end gap-2'>
|
||||
<div className='flex flex-row items-center gap-2'>
|
||||
<Button className='min-w-24' onClick={handleSubmit}>
|
||||
Cari
|
||||
</Button>
|
||||
<Button
|
||||
className='min-w-24'
|
||||
color='warning'
|
||||
onClick={resetFilters}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown
|
||||
trigger={
|
||||
<Button
|
||||
color='success'
|
||||
className='min-w-24'
|
||||
isLoading={isAnyExportLoading}
|
||||
onClick={() => {
|
||||
setDropdownOpen(!dropdownOpen);
|
||||
}}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
}
|
||||
align='end'
|
||||
direction='bottom'
|
||||
open={dropdownOpen}
|
||||
>
|
||||
<Menu className='w-32'>
|
||||
<MenuItem title='Excel' onClick={handleExportExcel} />
|
||||
<MenuItem title='PDF' onClick={handleExportPdf} />
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Lokasi'
|
||||
options={optionsLocation}
|
||||
isLoading={isLoadingLocation}
|
||||
placeholder='Lokasi'
|
||||
value={selectedLocation}
|
||||
onChange={locationChangeHandler}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Kandang'
|
||||
options={optionsKandang}
|
||||
isLoading={isLoadingKandang}
|
||||
placeholder='Kandang'
|
||||
value={selectedKandang}
|
||||
onChange={kandangChangeHandler}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Supplier'
|
||||
options={optionsSupplier}
|
||||
isLoading={isLoadingSupplier}
|
||||
placeholder='Supplier'
|
||||
value={selectedSupplier}
|
||||
onChange={supplierChangeHandler}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Produk'
|
||||
options={optionsNonstock}
|
||||
isLoading={isLoadingNonstock}
|
||||
placeholder='Produk'
|
||||
value={selectedNonstock}
|
||||
onChange={nonstockChangeHandler}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Kategori'
|
||||
options={categoryOptions}
|
||||
placeholder='Kategori'
|
||||
value={selectedCategory}
|
||||
onChange={categoryChangeHandler}
|
||||
/>
|
||||
<DateInput
|
||||
label='Tanggal Realisasi'
|
||||
value={filterState.realization_date}
|
||||
onChange={realizationDateChangeHandler}
|
||||
name='realization_date'
|
||||
placeholder='Tanggal Realisasi'
|
||||
/>
|
||||
<DebouncedTextInput
|
||||
label='Cari'
|
||||
name='search'
|
||||
value={filterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
placeholder='Cari'
|
||||
startAdornment={<Icon icon='mdi:magnify' width={24} height={24} />}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* ===== TABLE CONTENT ===== */}
|
||||
{!isSubmitted ? (
|
||||
<div className='mt-6 text-center text-gray-500'>
|
||||
Silakan pilih filter dan klik tombol Cari untuk menampilkan data.
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
) : data.length === 0 ? (
|
||||
<div className='mt-6 text-center text-gray-500'>
|
||||
Tidak ada data yang dapat ditampilkan...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Table<ReportExpense>
|
||||
columns={columns}
|
||||
data={data}
|
||||
pageSize={10}
|
||||
className={{
|
||||
containerClassName: 'mb-0',
|
||||
headerRowClassName: cn(
|
||||
TABLE_DEFAULT_STYLING,
|
||||
'whitespace-nowrap'
|
||||
),
|
||||
bodyRowClassName: cn(TABLE_DEFAULT_STYLING, 'whitespace-nowrap'),
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
{meta && (
|
||||
<div className='mt-6'>
|
||||
<Pagination
|
||||
currentPage={meta.page}
|
||||
totalItems={meta.total_results}
|
||||
onPageChange={handlePageChange}
|
||||
onRowChange={handleRowChange}
|
||||
onNextPage={handleNextPage}
|
||||
onPrevPage={handlePrevPage}
|
||||
rowOptions={[10, 25, 50, 100]}
|
||||
itemsPerPage={meta.limit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportExpenseTable;
|
||||
@@ -1,218 +0,0 @@
|
||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable, { UserOptions } from 'jspdf-autotable';
|
||||
interface jsPDFWithAutoTable extends jsPDF {
|
||||
lastAutoTable: {
|
||||
finalY: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PDFParams {
|
||||
location_name?: string;
|
||||
supplier_name?: string;
|
||||
realization_date?: string;
|
||||
}
|
||||
|
||||
const getStatusColor = (action?: string): [number, number, number] => {
|
||||
switch (action) {
|
||||
case 'APPROVED':
|
||||
case 'Selesai': // Berdasarkan data sumber
|
||||
return [220, 252, 231]; // Hijau muda (#dcfce7)
|
||||
case 'REJECTED':
|
||||
return [254, 226, 226]; // Merah muda (#fee2e2)
|
||||
case 'Realisasi': // Berdasarkan data sumber
|
||||
return [254, 243, 199]; // Kuning/Amber muda (#fef3c7)
|
||||
default:
|
||||
return [255, 255, 255]; // Putih
|
||||
}
|
||||
};
|
||||
|
||||
export const generateReportExpensePDF = async (
|
||||
data: ReportExpense[],
|
||||
params: PDFParams
|
||||
): Promise<void> => {
|
||||
// Inisialisasi dokumen dengan tipe yang sudah diekstensi
|
||||
const doc = new jsPDF('l', 'mm', 'a4') as jsPDFWithAutoTable;
|
||||
const pageWidth: number = doc.internal.pageSize.getWidth();
|
||||
const marginX: number = 14;
|
||||
|
||||
// --- HEADER SECTION ---
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(18);
|
||||
doc.setTextColor(31, 116, 191); // #1f74bf sesuai style
|
||||
doc.text('PT LUMBUNG TELUR INDONESIA', marginX, 20);
|
||||
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(102, 102, 102);
|
||||
doc.text(
|
||||
'SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Cipedes, Kec. Sukajadi, Kota Bandung 40162',
|
||||
marginX,
|
||||
25
|
||||
);
|
||||
|
||||
doc.setDrawColor(0);
|
||||
doc.line(marginX, 28, pageWidth - marginX, 28);
|
||||
|
||||
// --- TITLE & INFO SECTION ---
|
||||
doc.setFontSize(18);
|
||||
doc.setTextColor(31, 116, 191);
|
||||
doc.text('LAPORAN BIAYA OPERASIONAL', marginX, 38);
|
||||
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(0);
|
||||
const infoX: number = pageWidth - marginX;
|
||||
doc.text(
|
||||
`Tanggal Cetak: ${formatDate(new Date(), 'DD MMM YYYY')}`,
|
||||
infoX,
|
||||
35,
|
||||
{ align: 'right' }
|
||||
);
|
||||
doc.text(`Total Data: ${data.length} transaksi`, infoX, 40, {
|
||||
align: 'right',
|
||||
});
|
||||
|
||||
// --- GROUPING LOGIC ---
|
||||
const groupedBySupplier = data.reduce(
|
||||
(acc, item) => {
|
||||
const supplierName: string = item.supplier?.name || 'Unknown Supplier';
|
||||
if (!acc[supplierName]) acc[supplierName] = [];
|
||||
acc[supplierName].push(item);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ReportExpense[]>
|
||||
);
|
||||
|
||||
let currentY: number = 50;
|
||||
|
||||
// --- RENDER TABLES PER SUPPLIER ---
|
||||
Object.entries(groupedBySupplier).forEach(([supplierName, items]) => {
|
||||
// Cek sisa ruang halaman sebelum cetak judul supplier
|
||||
if (currentY > 180) {
|
||||
doc.addPage();
|
||||
currentY = 20;
|
||||
}
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(31, 116, 191);
|
||||
doc.text(supplierName, marginX, currentY);
|
||||
currentY += 5;
|
||||
|
||||
const tableOptions: UserOptions = {
|
||||
startY: currentY,
|
||||
head: [
|
||||
[
|
||||
{ content: 'No', rowSpan: 2 },
|
||||
{ content: 'No. PO', rowSpan: 2 },
|
||||
{ content: 'No. Referensi', rowSpan: 2 },
|
||||
{ content: 'Tgl Realisasi', rowSpan: 2 },
|
||||
{ content: 'Tgl Transaksi', rowSpan: 2 },
|
||||
{ content: 'Kategori', rowSpan: 2 },
|
||||
{ content: 'Produk', rowSpan: 2 },
|
||||
{ content: 'Lokasi', rowSpan: 2 },
|
||||
{ content: 'Kandang', rowSpan: 2 },
|
||||
{ content: 'Pengajuan', colSpan: 3, styles: { halign: 'center' } },
|
||||
{ content: 'Realisasi', colSpan: 3, styles: { halign: 'center' } },
|
||||
{ content: 'Status BOP', rowSpan: 2 },
|
||||
],
|
||||
['Qty', 'Harga', 'Total', 'Qty', 'Harga', 'Total'],
|
||||
],
|
||||
body: items.map((item, index) => {
|
||||
const pQty: number = item.pengajuan?.qty || 0;
|
||||
const pPrice: number = item.pengajuan?.price || 0;
|
||||
const rQty: number = item.realisasi?.qty || 0;
|
||||
const rPrice: number = item.realisasi?.price || 0;
|
||||
|
||||
return [
|
||||
index + 1,
|
||||
item.po_number || '-',
|
||||
item.reference_number || '-',
|
||||
formatDate(item.realization_date, 'DD MMM YY'),
|
||||
formatDate(item.transaction_date, 'DD MMM YY'),
|
||||
item.category?.replace('-', ' ') || '-',
|
||||
item.pengajuan?.nonstock?.name || '-',
|
||||
item.kandang?.location?.name || '-',
|
||||
item.kandang?.name || '-',
|
||||
pQty.toLocaleString('id-ID'),
|
||||
formatCurrency(pPrice),
|
||||
formatCurrency(pQty * pPrice),
|
||||
rQty.toLocaleString('id-ID'),
|
||||
formatCurrency(rPrice),
|
||||
formatCurrency(rQty * rPrice),
|
||||
item.latest_approval?.step_name || '-',
|
||||
];
|
||||
}),
|
||||
theme: 'grid',
|
||||
styles: { fontSize: 6, cellPadding: 1.5, overflow: 'linebreak' },
|
||||
headStyles: {
|
||||
fillColor: [245, 245, 245],
|
||||
textColor: 0,
|
||||
fontStyle: 'bold',
|
||||
lineWidth: 0.1,
|
||||
},
|
||||
// HOOK UNTUK BADGE:
|
||||
didParseCell: (dataCell) => {
|
||||
// Index kolom 15 adalah Status BOP (berdasarkan array di atas)
|
||||
if (dataCell.section === 'body' && dataCell.column.index === 15) {
|
||||
const statusText = dataCell.cell.raw as string;
|
||||
|
||||
// Berikan warna latar belakang sel sesuai status
|
||||
dataCell.cell.styles.fillColor = getStatusColor(statusText);
|
||||
dataCell.cell.styles.textColor = [0, 0, 0]; // Teks hitam agar terbaca
|
||||
dataCell.cell.styles.fontStyle = 'bold';
|
||||
dataCell.cell.styles.halign = 'center';
|
||||
}
|
||||
},
|
||||
margin: { left: marginX, right: marginX },
|
||||
};
|
||||
|
||||
autoTable(doc, tableOptions);
|
||||
currentY = doc.lastAutoTable.finalY + 10;
|
||||
});
|
||||
|
||||
// --- GRAND TOTAL SECTION ---
|
||||
const grandTotals = data.reduce(
|
||||
(acc, item) => {
|
||||
const pTotal = (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0);
|
||||
const rTotal = (item.realisasi?.qty || 0) * (item.realisasi?.price || 0);
|
||||
return {
|
||||
pengajuan: acc.pengajuan + pTotal,
|
||||
realisasi: acc.realisasi + rTotal,
|
||||
};
|
||||
},
|
||||
{ pengajuan: 0, realisasi: 0 }
|
||||
);
|
||||
|
||||
if (currentY > 250) {
|
||||
doc.addPage();
|
||||
currentY = 20;
|
||||
}
|
||||
|
||||
autoTable(doc, {
|
||||
startY: currentY,
|
||||
body: [
|
||||
['GRAND TOTAL PENGAJUAN', formatCurrency(grandTotals.pengajuan)],
|
||||
['GRAND TOTAL REALISASI', formatCurrency(grandTotals.realisasi)],
|
||||
],
|
||||
styles: { fontSize: 8, fontStyle: 'bold' },
|
||||
columnStyles: {
|
||||
0: { cellWidth: 50, fillColor: [245, 245, 245] },
|
||||
1: { cellWidth: 50 },
|
||||
},
|
||||
theme: 'grid',
|
||||
margin: { left: marginX },
|
||||
});
|
||||
|
||||
// --- FOOTER ---
|
||||
const finalY: number = doc.lastAutoTable.finalY + 20;
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(31, 116, 191);
|
||||
doc.text('PT LUMBUNG TELUR INDONESIA', pageWidth - marginX, finalY, {
|
||||
align: 'right',
|
||||
});
|
||||
|
||||
// Download File
|
||||
const fileName: string = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
|
||||
doc.save(fileName);
|
||||
};
|
||||
@@ -1,365 +0,0 @@
|
||||
import { StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 18,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
logo: {
|
||||
width: 120,
|
||||
height: 30,
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyInfo: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
address: {
|
||||
fontSize: 7,
|
||||
color: '#666666',
|
||||
maxWidth: 400,
|
||||
marginBottom: 10,
|
||||
},
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
marginBottom: 15,
|
||||
},
|
||||
titleSection: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 20,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
flex: 3,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
poInfo: {
|
||||
flex: 1,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
table: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
marginBottom: 15,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
tableCellLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellHeaderLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellNarrow: {
|
||||
width: '1%',
|
||||
minWidth: 20,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellNarrowHeader: {
|
||||
width: '1%',
|
||||
minWidth: 20,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellWrap: {
|
||||
flex: 1,
|
||||
maxWidth: 80,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
tableCellWrapHeader: {
|
||||
flex: 1,
|
||||
maxWidth: 80,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
// Nested header styles
|
||||
tableHeaderGroup: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableHeaderGroupLast: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableHeaderGroupTitle: {
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
tableSubHeaderRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
// Specific width columns
|
||||
tableCellXSmall: {
|
||||
width: 30,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
tableCellXSmallHeader: {
|
||||
width: 30,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellSmall: {
|
||||
width: 40,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
tableCellSmallHeader: {
|
||||
width: 40,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellMedium: {
|
||||
width: 60,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
tableCellMediumHeader: {
|
||||
width: 60,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellRightXSmall: {
|
||||
width: 30,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellRightSmall: {
|
||||
width: 40,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellRightMedium: {
|
||||
width: 60,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
grandTotalRow: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#000000',
|
||||
borderTopStyle: 'solid',
|
||||
},
|
||||
grandTotalLabel: {
|
||||
flex: 3,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
},
|
||||
grandTotalValue: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
allocationSection: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
allocationTable: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerTable: {
|
||||
marginTop: 5,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
innerCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
innerCellLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
innerCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
innerCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
footer: {
|
||||
marginTop: 30,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
footerCompany: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
flex: 1,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
specialInstructionTable: {
|
||||
width: '60%',
|
||||
maxWidth: 300,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default pdfStyles;
|
||||
@@ -1,32 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Tabs from '@/components/Tabs';
|
||||
import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab';
|
||||
|
||||
const LogisticStockTabs = () => {
|
||||
const tabs = [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Rekapitulasi Pembelian Per Supplier',
|
||||
content: <PurchasesPerSupplierTab />,
|
||||
},
|
||||
// {
|
||||
// id: '2',
|
||||
// label: 'Rekapitulasi Pemakaian Barang',
|
||||
// content: 'Rekapitulasi Pemakaian Barang Tab',
|
||||
// },
|
||||
// {
|
||||
// id: '3',
|
||||
// label: 'Rekapitulasi Stock Persediaan Barang',
|
||||
// content: 'Rekapitulasi Stock Persediaan Barang Tab',
|
||||
// },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<Tabs tabs={tabs} variant='lifted' />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogisticStockTabs;
|
||||
@@ -1,404 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
Document,
|
||||
StyleSheet,
|
||||
Font,
|
||||
pdf,
|
||||
} from '@react-pdf/renderer';
|
||||
import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
src: 'helvetica',
|
||||
});
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
titleSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
mainTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 5,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
supplierTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
table: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
marginBottom: 15,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
textAlign: 'left',
|
||||
},
|
||||
tableCellNo: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellLast: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
paddingVertical: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellHeaderRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
textAlign: 'right',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
tableCellHeaderLast: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
paddingVertical: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellCenter: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellCenterLast: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
supplierSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
supplierSectionBreak: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: '#1f74bf',
|
||||
color: '#FFFFFF',
|
||||
padding: 2,
|
||||
borderRadius: 2,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
alignSelf: 'center',
|
||||
marginRight: 4,
|
||||
},
|
||||
parameterBadge: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
color: '#333333',
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
fontSize: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
parameterContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: 8,
|
||||
},
|
||||
});
|
||||
|
||||
interface PurchasesPerSupplierExportParams {
|
||||
data: LogisticPurchasePerSupplierReport[];
|
||||
params: {
|
||||
area_name?: string;
|
||||
supplier_name?: string;
|
||||
product_name?: string;
|
||||
product_category_name?: string;
|
||||
received_date?: string;
|
||||
po_date?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
sort_by?: string;
|
||||
filter_by?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const getParameterText = (
|
||||
params: PurchasesPerSupplierExportParams['params']
|
||||
) => {
|
||||
const paramsText = [];
|
||||
|
||||
if (params.supplier_name) {
|
||||
paramsText.push(`Supplier: ${params.supplier_name}`);
|
||||
} else {
|
||||
paramsText.push('Semua Supplier');
|
||||
}
|
||||
|
||||
if (params.start_date && params.end_date) {
|
||||
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||
const endDate = formatDate(params.end_date, 'DD MMM YYYY');
|
||||
paramsText.push(`Periode: ${startDate} - ${endDate}`);
|
||||
} else if (params.start_date) {
|
||||
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||
paramsText.push(`Tanggal: ${startDate}`);
|
||||
}
|
||||
|
||||
const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm');
|
||||
paramsText.push(`Dicetak: ${currentDate}`);
|
||||
|
||||
return paramsText;
|
||||
};
|
||||
|
||||
const createPDFDocument = (
|
||||
supplierReports: LogisticPurchasePerSupplierReport[],
|
||||
params: PurchasesPerSupplierExportParams['params']
|
||||
) => (
|
||||
<Document>
|
||||
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
||||
{/* Title and Parameters */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.mainTitle}>
|
||||
Laporan > Rekapitulasi Pembelian Per Supplier
|
||||
</Text>
|
||||
<View style={pdfStyles.parameterContainer}>
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
<Text>
|
||||
Jenis Tanggal:{' '}
|
||||
{params.filter_by === 'received_date'
|
||||
? 'Tanggal Terima'
|
||||
: 'Tanggal PO'}
|
||||
</Text>
|
||||
</View>
|
||||
{getParameterText(params).map((param, index) => (
|
||||
<View key={index} style={pdfStyles.parameterBadge}>
|
||||
<Text>{param}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Supplier Sections */}
|
||||
{supplierReports.map(
|
||||
(
|
||||
supplierReport: LogisticPurchasePerSupplierReport,
|
||||
supplierIndex: number
|
||||
) => {
|
||||
return (
|
||||
<View
|
||||
key={supplierReport.supplier.id}
|
||||
style={[
|
||||
pdfStyles.supplierSection,
|
||||
supplierIndex < supplierReports.length - 1
|
||||
? pdfStyles.supplierSectionBreak
|
||||
: {},
|
||||
]}
|
||||
>
|
||||
<Text style={pdfStyles.supplierTitle}>
|
||||
{supplierReport.supplier.name}
|
||||
</Text>
|
||||
|
||||
<View style={pdfStyles.table}>
|
||||
{/* Table Header */}
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
|
||||
<Text>No</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Tanggal Terima</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Tanggal PO</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Referensi</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Produk</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Tujuan</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||
<Text>Qty</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Harga Beli</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||
<Text>Nilai Pembelian</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Biaya Transport</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||
<Text>Total</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||
<Text>Armada</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Surat Jalan</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Table Body */}
|
||||
{supplierReport.rows.map(
|
||||
(
|
||||
item: LogisticPurchasePerSupplierReport['rows'][number],
|
||||
index: number
|
||||
) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
index < supplierReport.rows.length - 1
|
||||
? pdfStyles.tableBorderBottom
|
||||
: {},
|
||||
]}
|
||||
>
|
||||
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||
<Text>{index + 1}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>
|
||||
{formatDate(item.receive_date, 'DD-MMM-YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{formatDate(item.po_date, 'DD-MMM-YYYY')}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.po_number || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.product?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.warehouse?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||
<Text>{formatNumber(item.qty || 0)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.unit_price || 0)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||
<Text>{formatCurrency(item.purchase_value || 0)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{formatCurrency(item.transport_unit_price || 0)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||
<Text>{formatCurrency(item.total_amount || 0)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
<View style={pdfStyles.badge}>
|
||||
<Text>{item.expedition || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text>{item.delivery_number || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
|
||||
export const generatePurchasesPerSupplierPDF = async (
|
||||
data: LogisticPurchasePerSupplierReport[],
|
||||
params: PurchasesPerSupplierExportParams['params']
|
||||
): Promise<void> => {
|
||||
const PDFDocument = createPDFDocument(data, params);
|
||||
|
||||
try {
|
||||
const blob = await pdf(PDFDocument).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,934 +0,0 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { ChangeEventHandler } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import Card from '@/components/Card';
|
||||
import SelectInput, {
|
||||
useSelect,
|
||||
OptionType,
|
||||
} from '@/components/input/SelectInput';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import { AreaApi } from '@/services/api/master-data';
|
||||
import { SupplierApi } from '@/services/api/master-data';
|
||||
import { ProductApi } from '@/services/api/master-data';
|
||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||
import { LogisticApi } from '@/services/api/report/logistic-stock';
|
||||
import Table from '@/components/Table';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
LogisticPurchasePerSupplierReport,
|
||||
LogisticPurchasePerSupplierSummary,
|
||||
} from '@/types/api/report/logistic-stock';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import Pagination from '@/components/Pagination';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport';
|
||||
import toast from 'react-hot-toast';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
const PurchasesPerSupplierTab = () => {
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||
|
||||
// ===== PAGINATION STATE =====
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
// ===== SUBMISSION STATE =====
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// ===== TABLE FILTER STATE =====
|
||||
const { state: tableFilterState, updateFilter } = useTableFilter({
|
||||
initial: {
|
||||
area_id: [] as string[],
|
||||
supplier_id: [] as string[],
|
||||
product_id: [] as string[],
|
||||
product_category_id: [] as string[],
|
||||
received_date: '',
|
||||
po_date: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
sort_by: '',
|
||||
filter_by: 'received_date',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
},
|
||||
});
|
||||
|
||||
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
||||
AreaApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
|
||||
useSelect(SupplierApi.basePath, 'id', 'name', 'search', {
|
||||
category: 'SAPRONAK',
|
||||
});
|
||||
|
||||
const { options: productOptions, isLoadingOptions: isLoadingProducts } =
|
||||
useSelect(ProductApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const {
|
||||
options: productCategoryOptions,
|
||||
isLoadingOptions: isLoadingProductCategories,
|
||||
} = useSelect(ProductCategoryApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const dataTypeOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'received_date', label: 'Tanggal Terima' },
|
||||
{ value: 'po_date', label: 'Tanggal PO' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const sortByOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'ASC', label: 'Ascending' },
|
||||
{ value: 'DESC', label: 'Descending' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const areaChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||
updateFilter(
|
||||
'area_id',
|
||||
arr.map((v) => String((v as OptionType).value))
|
||||
);
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const supplierChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||
updateFilter(
|
||||
'supplier_id',
|
||||
arr.map((v) => String((v as OptionType).value))
|
||||
);
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const productChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||
updateFilter(
|
||||
'product_id',
|
||||
arr.map((v) => String((v as OptionType).value))
|
||||
);
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const productCategoryChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||
updateFilter(
|
||||
'product_category_id',
|
||||
arr.map((v) => String((v as OptionType).value))
|
||||
);
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const dataTypeChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const newVal = val as OptionType;
|
||||
const filterValue =
|
||||
(newVal?.value as 'received_date' | 'po_date') || 'received_date';
|
||||
updateFilter('filter_by', filterValue);
|
||||
updateFilter('received_date', '');
|
||||
updateFilter('po_date', '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const sortByHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const newVal = val as OptionType;
|
||||
const sortValue = (newVal?.value as 'ASC' | 'DESC') || 'ASC';
|
||||
updateFilter('sort_by', sortValue);
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const startDateChangeHandler = useCallback<
|
||||
ChangeEventHandler<HTMLInputElement>
|
||||
>(
|
||||
(e) => {
|
||||
const val = e.target.value;
|
||||
updateFilter('start_date', val || '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const endDateChangeHandler = useCallback<
|
||||
ChangeEventHandler<HTMLInputElement>
|
||||
>(
|
||||
(e) => {
|
||||
const val = e.target.value;
|
||||
updateFilter('end_date', val || '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const resetFilters = useCallback(() => {
|
||||
updateFilter('area_id', []);
|
||||
updateFilter('supplier_id', []);
|
||||
updateFilter('product_id', []);
|
||||
updateFilter('product_category_id', []);
|
||||
updateFilter('received_date', '');
|
||||
updateFilter('po_date', '');
|
||||
updateFilter('start_date', '');
|
||||
updateFilter('end_date', '');
|
||||
updateFilter('sort_by', '');
|
||||
updateFilter('filter_by', 'received_date');
|
||||
setIsSubmitted(false);
|
||||
}, [updateFilter]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
setIsSubmitted(true);
|
||||
setCurrentPage(1);
|
||||
}, []);
|
||||
|
||||
// ===== DATA FETCHING =====
|
||||
const { data: purchasePerSupplier, isLoading } = useSWR(
|
||||
isSubmitted
|
||||
? () => {
|
||||
const params = {
|
||||
area_id:
|
||||
tableFilterState.area_id.length > 0
|
||||
? tableFilterState.area_id.join(',')
|
||||
: undefined,
|
||||
supplier_id:
|
||||
tableFilterState.supplier_id.length > 0
|
||||
? tableFilterState.supplier_id.join(',')
|
||||
: undefined,
|
||||
product_id:
|
||||
tableFilterState.product_id.length > 0
|
||||
? tableFilterState.product_id.join(',')
|
||||
: undefined,
|
||||
product_category_id:
|
||||
tableFilterState.product_category_id.length > 0
|
||||
? tableFilterState.product_category_id.join(',')
|
||||
: undefined,
|
||||
received_date:
|
||||
tableFilterState.filter_by === 'received_date'
|
||||
? tableFilterState.start_date || undefined
|
||||
: undefined,
|
||||
po_date:
|
||||
tableFilterState.filter_by === 'po_date'
|
||||
? tableFilterState.start_date || undefined
|
||||
: undefined,
|
||||
start_date: tableFilterState.start_date || undefined,
|
||||
end_date: tableFilterState.end_date || undefined,
|
||||
sort_by: tableFilterState.sort_by || undefined,
|
||||
filter_by: tableFilterState.filter_by || undefined,
|
||||
page: currentPage,
|
||||
limit: pageSize,
|
||||
};
|
||||
|
||||
return ['logistic-purchase-report', params];
|
||||
}
|
||||
: null,
|
||||
([, params]) =>
|
||||
LogisticApi.getLogisticPurchasePerSupplierReport(
|
||||
params.area_id,
|
||||
params.supplier_id,
|
||||
params.product_id,
|
||||
params.product_category_id,
|
||||
params.received_date,
|
||||
params.po_date,
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
params.sort_by,
|
||||
params.filter_by,
|
||||
params.page,
|
||||
params.limit
|
||||
)
|
||||
);
|
||||
|
||||
const data: LogisticPurchasePerSupplierReport[] = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(purchasePerSupplier)
|
||||
? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplierReport[]) ||
|
||||
[]
|
||||
: [],
|
||||
[purchasePerSupplier]
|
||||
);
|
||||
|
||||
const meta =
|
||||
isResponseSuccess(purchasePerSupplier) && purchasePerSupplier?.meta
|
||||
? purchasePerSupplier.meta
|
||||
: null;
|
||||
|
||||
// ===== EXPORT DATA FETCHER =====
|
||||
const logisticPurchasePerSupplierExport = useCallback(async (): Promise<
|
||||
LogisticPurchasePerSupplierReport[] | null
|
||||
> => {
|
||||
const params = {
|
||||
area_id:
|
||||
tableFilterState.area_id.length > 0
|
||||
? tableFilterState.area_id.join(',')
|
||||
: undefined,
|
||||
supplier_id:
|
||||
tableFilterState.supplier_id.length > 0
|
||||
? tableFilterState.supplier_id.join(',')
|
||||
: undefined,
|
||||
product_id:
|
||||
tableFilterState.product_id.length > 0
|
||||
? tableFilterState.product_id.join(',')
|
||||
: undefined,
|
||||
product_category_id:
|
||||
tableFilterState.product_category_id.length > 0
|
||||
? tableFilterState.product_category_id.join(',')
|
||||
: undefined,
|
||||
received_date:
|
||||
tableFilterState.filter_by === 'received_date'
|
||||
? tableFilterState.start_date || undefined
|
||||
: undefined,
|
||||
po_date:
|
||||
tableFilterState.filter_by === 'po_date'
|
||||
? tableFilterState.start_date || undefined
|
||||
: undefined,
|
||||
start_date: tableFilterState.start_date || undefined,
|
||||
end_date: tableFilterState.end_date || undefined,
|
||||
sort_by: tableFilterState.sort_by || undefined,
|
||||
filter_by: tableFilterState.filter_by || undefined,
|
||||
limit: 100,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
const response = await LogisticApi.getLogisticPurchasePerSupplierReport(
|
||||
params.area_id,
|
||||
params.supplier_id,
|
||||
params.product_id,
|
||||
params.product_category_id,
|
||||
params.received_date,
|
||||
params.po_date,
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
params.sort_by,
|
||||
params.filter_by,
|
||||
params.page,
|
||||
params.limit
|
||||
);
|
||||
|
||||
return isResponseSuccess(response)
|
||||
? (response.data as unknown as LogisticPurchasePerSupplierReport[])
|
||||
: null;
|
||||
}, [tableFilterState]);
|
||||
|
||||
// ===== EXPORT HANDLERS =====
|
||||
const handleExportExcel = useCallback(async () => {
|
||||
setIsExcelExportLoading(true);
|
||||
try {
|
||||
const allDataForExport = await logisticPurchasePerSupplierExport();
|
||||
|
||||
if (
|
||||
!allDataForExport ||
|
||||
!Array.isArray(allDataForExport) ||
|
||||
allDataForExport.length === 0
|
||||
) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
allDataForExport.forEach((supplierReport) => {
|
||||
const supplierData = supplierReport.rows;
|
||||
const supplierName =
|
||||
supplierReport.supplier?.name || 'Unknown Supplier';
|
||||
|
||||
const excelData: { [key: string]: string | number }[] =
|
||||
supplierData.map((item, index) => ({
|
||||
No: index + 1,
|
||||
'Tanggal Terima': item.receive_date
|
||||
? formatDate(item.receive_date, 'DD MMM YYYY')
|
||||
: '',
|
||||
'Tanggal PO': item.po_date
|
||||
? formatDate(item.po_date, 'DD MMM YYYY')
|
||||
: '',
|
||||
'No. Referensi': item.po_number || '',
|
||||
'Nama Produk': item.product?.name || '',
|
||||
Tujuan: item.warehouse?.name || '',
|
||||
QTY: item.qty || 0,
|
||||
'Harga Beli (Rp)': item.unit_price || 0,
|
||||
'Value Harga Beli (Rp)': item.purchase_value || 0,
|
||||
'Transport (Rp)': item.transport_unit_price || 0,
|
||||
'Value Transport (Rp)': item.transport_value || 0,
|
||||
'Jumlah (Rp)': item.total_amount || 0,
|
||||
Ekspedisi: item.expedition || '',
|
||||
'Surat Jalan': item.delivery_number || '',
|
||||
}));
|
||||
|
||||
if (supplierReport.summary) {
|
||||
excelData.push({
|
||||
No: 'Total',
|
||||
'Tanggal Terima': '',
|
||||
'Tanggal PO': '',
|
||||
'No. Referensi': '',
|
||||
'Nama Produk': '',
|
||||
Tujuan: '',
|
||||
QTY: supplierReport.summary.total_qty || 0,
|
||||
'Harga Beli (Rp)': '',
|
||||
'Value Harga Beli (Rp)':
|
||||
supplierReport.summary.total_purchase_value || 0,
|
||||
'Transport (Rp)': '',
|
||||
'Value Transport (Rp)':
|
||||
supplierReport.summary.total_transport_value || 0,
|
||||
'Jumlah (Rp)': supplierReport.summary.total_amount || 0,
|
||||
Ekspedisi: '',
|
||||
'Surat Jalan': '',
|
||||
});
|
||||
}
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
||||
|
||||
const colWidths = [
|
||||
{ wch: 5 }, // No
|
||||
{ wch: 15 }, // Tanggal Terima
|
||||
{ wch: 15 }, // Tanggal PO
|
||||
{ wch: 15 }, // No. Referensi
|
||||
{ wch: 30 }, // Nama Produk
|
||||
{ wch: 20 }, // Tujuan
|
||||
{ wch: 10 }, // QTY
|
||||
{ wch: 18 }, // Harga Beli
|
||||
{ wch: 20 }, // Value Harga Beli
|
||||
{ wch: 15 }, // Transport
|
||||
{ wch: 20 }, // Value Transport
|
||||
{ wch: 18 }, // Jumlah
|
||||
{ wch: 15 }, // Ekspedisi
|
||||
{ wch: 15 }, // Surat Jalan
|
||||
];
|
||||
worksheet['!cols'] = colWidths;
|
||||
|
||||
const sheetName =
|
||||
supplierName.length > 31
|
||||
? supplierName.substring(0, 31)
|
||||
: supplierName;
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||
});
|
||||
|
||||
const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`;
|
||||
|
||||
XLSX.writeFile(workbook, filename);
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
logisticPurchasePerSupplierExport,
|
||||
tableFilterState,
|
||||
areaOptions,
|
||||
supplierOptions,
|
||||
]);
|
||||
|
||||
const handleExportPdf = useCallback(async () => {
|
||||
setIsPdfExportLoading(true);
|
||||
try {
|
||||
const allDataForExport = await logisticPurchasePerSupplierExport();
|
||||
|
||||
if (
|
||||
!allDataForExport ||
|
||||
!Array.isArray(allDataForExport) ||
|
||||
allDataForExport.length === 0
|
||||
) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
|
||||
const areaName =
|
||||
tableFilterState.area_id.length > 0
|
||||
? tableFilterState.area_id
|
||||
.map(
|
||||
(id) =>
|
||||
areaOptions.find((opt) => opt.value === Number(id))?.label
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(', ') || 'Semua Area'
|
||||
: 'Semua Area';
|
||||
|
||||
const supplierName =
|
||||
tableFilterState.supplier_id.length > 0
|
||||
? tableFilterState.supplier_id
|
||||
.map(
|
||||
(id) =>
|
||||
supplierOptions.find((opt) => opt.value === Number(id))?.label
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(', ') || 'Semua Supplier'
|
||||
: 'Semua Supplier';
|
||||
|
||||
const productName =
|
||||
tableFilterState.product_id.length > 0
|
||||
? tableFilterState.product_id
|
||||
.map(
|
||||
(id) =>
|
||||
productOptions.find((opt) => opt.value === Number(id))?.label
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(', ') || 'Semua Produk'
|
||||
: 'Semua Produk';
|
||||
|
||||
const productCategoryName =
|
||||
tableFilterState.product_category_id.length > 0
|
||||
? tableFilterState.product_category_id
|
||||
.map(
|
||||
(id) =>
|
||||
productCategoryOptions.find((opt) => opt.value === Number(id))
|
||||
?.label
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(', ') || 'Semua Kategori Produk'
|
||||
: 'Semua Kategori Produk';
|
||||
|
||||
const exportParams = {
|
||||
area_name: areaName,
|
||||
supplier_name: supplierName,
|
||||
product_name: productName,
|
||||
product_category_name: productCategoryName,
|
||||
filter_by: tableFilterState.filter_by || 'received_date',
|
||||
start_date: tableFilterState.start_date || '',
|
||||
end_date: tableFilterState.end_date || '',
|
||||
};
|
||||
|
||||
await generatePurchasesPerSupplierPDF(allDataForExport, exportParams);
|
||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsPdfExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
logisticPurchasePerSupplierExport,
|
||||
tableFilterState,
|
||||
areaOptions,
|
||||
supplierOptions,
|
||||
productOptions,
|
||||
productCategoryOptions,
|
||||
]);
|
||||
|
||||
// ===== PAGINATION HANDLERS =====
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handleRowChange = (pageSize: number) => {
|
||||
setPageSize(pageSize);
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (meta && currentPage < meta.total_pages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const getTableColumns = (
|
||||
summary: LogisticPurchasePerSupplierSummary
|
||||
): ColumnDef<LogisticPurchasePerSupplierReport['rows'][0]>[] => {
|
||||
const tableColumns: ColumnDef<
|
||||
LogisticPurchasePerSupplierReport['rows'][0]
|
||||
>[] = [
|
||||
{
|
||||
id: 'no',
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
footer: () => <div className='font-semibold text-gray-900'>Total</div>,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'received_date',
|
||||
header: 'Tanggal Terima',
|
||||
accessorKey: 'receive_date',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.receive_date;
|
||||
return formatDate(value, 'DD MMM YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'po_date',
|
||||
header: 'Tanggal PO',
|
||||
accessorKey: 'po_date',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.po_date;
|
||||
return formatDate(value, 'DD MMM YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'po_number',
|
||||
header: 'No. Referensi',
|
||||
accessorKey: 'po_number',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.po_number;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'product_name',
|
||||
header: 'Nama Produk',
|
||||
accessorKey: 'product.name',
|
||||
cell: (props) => {
|
||||
const product = props.row.original.product;
|
||||
return product?.name || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'destination_warehouse',
|
||||
header: 'Tujuan',
|
||||
accessorKey: 'warehouse.name',
|
||||
cell: (props) => {
|
||||
const warehouse = props.row.original.warehouse;
|
||||
return warehouse?.name || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'qty',
|
||||
header: 'QTY',
|
||||
accessorKey: 'qty',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.qty;
|
||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatNumber(summary.total_qty) || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'price',
|
||||
header: 'Harga Beli (Rp)',
|
||||
accessorKey: 'unit_price',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.unit_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summary.total_unit_price) || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'purchase_amount',
|
||||
header: 'Value Harga Beli (Rp)',
|
||||
accessorKey: 'purchase_value',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.purchase_value;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summary.total_purchase_value) || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'transport',
|
||||
header: 'Transport (Rp)',
|
||||
accessorKey: 'transport_unit_price',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.transport_unit_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summary.total_transport_unit_price) || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'value_transport',
|
||||
header: 'Value Transport (Rp)',
|
||||
accessorKey: 'transport_value',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.transport_value;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summary.total_transport_value) || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'total',
|
||||
header: 'Jumlah (Rp)',
|
||||
accessorKey: 'total_amount',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.total_amount;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summary.total_amount) || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'expedition_vendor_name',
|
||||
header: 'Ekspedisi',
|
||||
accessorKey: 'expedition',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.expedition;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'travel_number',
|
||||
header: 'Surat Jalan',
|
||||
accessorKey: 'delivery_number',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.delivery_number;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
return tableColumns;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full p-0 sm:p-4'>
|
||||
<Card
|
||||
subtitle='Laporan > Rekapitulasi Pembelian Per Supplier'
|
||||
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
||||
>
|
||||
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||
<Button color='primary' onClick={handleSubmit}>
|
||||
Cari
|
||||
</Button>
|
||||
<Button color='warning' onClick={resetFilters}>
|
||||
Reset
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger={
|
||||
<Button color='success' isLoading={isAnyExportLoading}>
|
||||
Export
|
||||
</Button>
|
||||
}
|
||||
align='end'
|
||||
>
|
||||
<Menu className='w-32'>
|
||||
<MenuItem title='Excel' onClick={handleExportExcel} />
|
||||
<MenuItem title='PDF' onClick={handleExportPdf} />
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||
<SelectInput
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
isMulti
|
||||
options={areaOptions}
|
||||
value={areaOptions.filter((opt) =>
|
||||
(tableFilterState.area_id || [])
|
||||
.map(String)
|
||||
.includes(String(opt.value))
|
||||
)}
|
||||
onChange={areaChangeHandler}
|
||||
isLoading={isLoadingAreas}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
label='Supplier'
|
||||
placeholder='Pilih Supplier'
|
||||
isMulti
|
||||
options={supplierOptions}
|
||||
value={supplierOptions.filter((opt) =>
|
||||
(tableFilterState.supplier_id || [])
|
||||
.map(String)
|
||||
.includes(String(opt.value))
|
||||
)}
|
||||
onChange={supplierChangeHandler}
|
||||
isLoading={isLoadingSuppliers}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
isMulti
|
||||
options={productOptions}
|
||||
value={productOptions.filter((opt) =>
|
||||
(tableFilterState.product_id || [])
|
||||
.map(String)
|
||||
.includes(String(opt.value))
|
||||
)}
|
||||
onChange={productChangeHandler}
|
||||
isLoading={isLoadingProducts}
|
||||
isClearable
|
||||
/>
|
||||
</div>
|
||||
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||
<SelectInput
|
||||
label='Kategori Produk'
|
||||
placeholder='Pilih Kategori Produk'
|
||||
isMulti
|
||||
options={productCategoryOptions}
|
||||
value={productCategoryOptions.filter((opt) =>
|
||||
(tableFilterState.product_category_id || [])
|
||||
.map(String)
|
||||
.includes(String(opt.value))
|
||||
)}
|
||||
onChange={productCategoryChangeHandler}
|
||||
isLoading={isLoadingProductCategories}
|
||||
isClearable
|
||||
/>
|
||||
<div className='md:flex md:flex-row grid grid-cols-1 gap-4'>
|
||||
<SelectInput
|
||||
label='Filter Berdasarkan'
|
||||
placeholder='Pilih Filter Berdasarkan'
|
||||
options={dataTypeOptions}
|
||||
value={
|
||||
dataTypeOptions?.find(
|
||||
(option) => option.value === tableFilterState.filter_by
|
||||
) || null
|
||||
}
|
||||
onChange={dataTypeChangeHandler}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
/>
|
||||
<SelectInput
|
||||
label='Urutkan Berdasarkan'
|
||||
placeholder='Urutkan Berdasarkan'
|
||||
options={sortByOptions}
|
||||
value={
|
||||
sortByOptions?.find(
|
||||
(option) => option.value === tableFilterState.sort_by
|
||||
) || null
|
||||
}
|
||||
onChange={sortByHandler}
|
||||
isLoading={false}
|
||||
isClearable={false}
|
||||
/>
|
||||
</div>
|
||||
<div className='md:flex md:flex-row grid grid-cols-1 gap-4'>
|
||||
<DateInput
|
||||
label='Tanggal Awal'
|
||||
name='start_date'
|
||||
placeholder='Pilih Tanggal Awal'
|
||||
value={tableFilterState.start_date}
|
||||
onChange={startDateChangeHandler}
|
||||
/>
|
||||
<DateInput
|
||||
label='Tanggal Akhir'
|
||||
name='end_date'
|
||||
placeholder='Pilih Tanggal Akhir'
|
||||
value={tableFilterState.end_date}
|
||||
onChange={endDateChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isSubmitted ? (
|
||||
<div className='mt-6 text-center text-gray-500'>
|
||||
Silakan pilih filter dan klik tombol Submit untuk menampilkan data.
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
) : data.length === 0 ? (
|
||||
<div className='mt-6 text-center text-gray-500'>
|
||||
Tidak ada data yang dapat ditampilkan...
|
||||
</div>
|
||||
) : (
|
||||
data.map((supplierReport) => {
|
||||
const summary = supplierReport.summary || {
|
||||
total_qty: 0,
|
||||
total_unit_price: 0,
|
||||
total_purchase_value: 0,
|
||||
total_transport_unit_price: 0,
|
||||
total_transport_value: 0,
|
||||
total_amount: 0,
|
||||
};
|
||||
|
||||
const totalPurchase = summary.total_amount;
|
||||
const tableColumns = getTableColumns(summary);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={supplierReport.supplier.id}
|
||||
title={supplierReport.supplier.name}
|
||||
subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
variant='bordered'
|
||||
collapsible={true}
|
||||
>
|
||||
<Table
|
||||
data={supplierReport.rows}
|
||||
columns={tableColumns}
|
||||
pageSize={10}
|
||||
renderFooter={supplierReport.rows.length > 0}
|
||||
className={{
|
||||
containerClassName: 'w-full',
|
||||
tableWrapperClassName: 'overflow-x-auto mt-4',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Card>
|
||||
{meta && data.length > 0 && (
|
||||
<div className='mt-6'>
|
||||
<Pagination
|
||||
currentPage={meta.page}
|
||||
totalItems={meta.total_results}
|
||||
onPageChange={handlePageChange}
|
||||
onRowChange={handleRowChange}
|
||||
onNextPage={handleNextPage}
|
||||
onPrevPage={handlePrevPage}
|
||||
rowOptions={[10, 25, 50, 100]}
|
||||
itemsPerPage={meta.limit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchasesPerSupplierTab;
|
||||
@@ -1,37 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Tabs from '@/components/Tabs';
|
||||
import HppPerKandangTab from '@/components/pages/report/sale/tab/HppPerKandangTab';
|
||||
|
||||
const SaleReportTabs = () => {
|
||||
const tabs = [
|
||||
// {
|
||||
// id: '1',
|
||||
// label: 'Penjualan Harian',
|
||||
// content: 'Penjualan Harian Tab',
|
||||
// },
|
||||
// {
|
||||
// id: '2',
|
||||
// label: 'Transaksi Penjualan DO',
|
||||
// content: 'Transaksi Penjualan DO Tab',
|
||||
// },
|
||||
// {
|
||||
// id: '3',
|
||||
// label: 'Perbandingan HPP Per Rentang BW',
|
||||
// content: 'Perbandingan HPP Per Rentang BW Tab',
|
||||
// },
|
||||
{
|
||||
id: '4',
|
||||
label: 'HPP Harian Kandang',
|
||||
content: <HppPerKandangTab />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<Tabs tabs={tabs} variant='lifted' />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaleReportTabs;
|
||||
@@ -1,497 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
Document,
|
||||
StyleSheet,
|
||||
Font,
|
||||
pdf,
|
||||
} from '@react-pdf/renderer';
|
||||
import {
|
||||
HppPerKandangReport,
|
||||
HppPerKandangRow,
|
||||
HppPerKandangPerWeightRange,
|
||||
} from '@/types/api/report/hpp-per-kandang';
|
||||
import { formatDate, formatNumber, formatCurrency } from '@/lib/helper';
|
||||
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
src: 'helvetica',
|
||||
});
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
titleSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
mainTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 5,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
supplierTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
table: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
marginBottom: 15,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
textAlign: 'left',
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
paddingVertical: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellHeaderRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
textAlign: 'right',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellCenter: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
supplierSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
supplierSectionBreak: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
parameterBadge: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
color: '#333333',
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
fontSize: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
parameterContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: 8,
|
||||
},
|
||||
});
|
||||
|
||||
interface HppPerKandangExportParams {
|
||||
data: HppPerKandangReport;
|
||||
params: {
|
||||
area_name?: string;
|
||||
location_name?: string;
|
||||
kandang_name?: string;
|
||||
period?: string;
|
||||
weight_min?: string;
|
||||
weight_max?: string;
|
||||
show_unrecorded?: string;
|
||||
sort_by?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const getParameterText = (params: HppPerKandangExportParams['params']) => {
|
||||
const paramsText = [];
|
||||
|
||||
if (params.area_name && params.area_name !== 'Semua Area') {
|
||||
paramsText.push(`Area: ${params.area_name}`);
|
||||
}
|
||||
|
||||
if (params.location_name && params.location_name !== 'Semua Lokasi') {
|
||||
paramsText.push(`Lokasi: ${params.location_name}`);
|
||||
}
|
||||
|
||||
if (params.kandang_name && params.kandang_name !== 'Semua Kandang') {
|
||||
paramsText.push(`Kandang: ${params.kandang_name}`);
|
||||
}
|
||||
|
||||
if (params.period) {
|
||||
const formattedDate = formatDate(params.period, 'DD MMM YYYY');
|
||||
paramsText.push(`Tanggal: ${formattedDate}`);
|
||||
}
|
||||
|
||||
if (params.weight_min || params.weight_max) {
|
||||
const weightRange =
|
||||
params.weight_min && params.weight_max
|
||||
? `${params.weight_min} - ${params.weight_max} kg`
|
||||
: params.weight_min
|
||||
? `≥ ${params.weight_min} kg`
|
||||
: `≤ ${params.weight_max} kg`;
|
||||
paramsText.push(`Rentang Bobot: ${weightRange}`);
|
||||
}
|
||||
|
||||
if (params.show_unrecorded === 'true') {
|
||||
paramsText.push('Tampilkan: Tanpa Recording');
|
||||
}
|
||||
|
||||
const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm');
|
||||
paramsText.push(`Dicetak: ${currentDate}`);
|
||||
|
||||
return paramsText;
|
||||
};
|
||||
|
||||
const createPDFDocument = (
|
||||
data: HppPerKandangExportParams['data'],
|
||||
params: HppPerKandangExportParams['params']
|
||||
) => {
|
||||
const rekapitulasiByWeightRange = data.summary?.per_weight_range || [];
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
||||
{/* Title and Parameters */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.mainTitle}>
|
||||
Laporan > HPP Harian Kandang
|
||||
</Text>
|
||||
<View style={pdfStyles.parameterContainer}>
|
||||
{getParameterText(params).map((param, index) => (
|
||||
<View key={index} style={pdfStyles.parameterBadge}>
|
||||
<Text>{param}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Rekapitulasi Section */}
|
||||
<View style={pdfStyles.supplierSection}>
|
||||
<Text style={pdfStyles.supplierTitle}>Rekapitulasi</Text>
|
||||
|
||||
<View style={pdfStyles.table}>
|
||||
{/* Table Header */}
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||
<Text>Rentang BW</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>Sisa Ekor</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>Sisa Kg</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>Produksi Telur (Butir)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>Produksi Telur (Kg)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||
<Text>Feed (Supplier)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||
<Text>DOC (Supplier)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Rata-Rata Harga DOC</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Nilai Nominal Telur</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>HPP Ayam</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>HPP Telur (RP/KG)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Nominal Sisa</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Table Body - Rekapitulasi */}
|
||||
{rekapitulasiByWeightRange.map(
|
||||
(group: HppPerKandangPerWeightRange, index: number) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
index < rekapitulasiByWeightRange.length - 1
|
||||
? pdfStyles.tableBorderBottom
|
||||
: {},
|
||||
]}
|
||||
>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
||||
<Text>{group.label}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatNumber(group.remaining_chicken_birds)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>
|
||||
{formatNumber(group.remaining_chicken_weight_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatNumber(group.avg_weight_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatNumber(group.egg_production_pieces)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatNumber(group.egg_production_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||
<Text>
|
||||
{group.feed_suppliers
|
||||
?.map(
|
||||
(s: { alias?: string; name: string }) =>
|
||||
s.alias || s.name
|
||||
)
|
||||
.join(' | ') || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{group.doc_suppliers
|
||||
?.map(
|
||||
(s: { alias?: string; name: string }) =>
|
||||
s.alias || s.name
|
||||
)
|
||||
.join(' | ') || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(group.average_doc_price_rp)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(group.egg_value_rp)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatCurrency(group.hpp_rp)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(group.remaining_value_rp)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Detail Per Kandang Section */}
|
||||
<View style={pdfStyles.supplierSectionBreak}>
|
||||
<Text style={pdfStyles.supplierTitle}>Detail Per Kandang</Text>
|
||||
|
||||
<View style={pdfStyles.table}>
|
||||
{/* Table Header */}
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
|
||||
<Text>No</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||
<Text>Kandang</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Rentang BW</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||
<Text>Sisa Ekor</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||
<Text>Sisa Kg (Ayam)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||
<Text>Produksi Telur (Butir)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||
<Text>Produksi Telur (Kg)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||
<Text>Feed (Supplier)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>DOC (Supplier)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Rata-Rata Harga DOC</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Nilai Nominal Telur</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||
<Text>HPP Ayam</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>HPP Telur (RP/KG)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Nominal Sisa</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Table Body - Detail Per Kandang */}
|
||||
{data.rows.map((item: HppPerKandangRow, index: number) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
index < data.rows.length - 1
|
||||
? pdfStyles.tableBorderBottom
|
||||
: {},
|
||||
]}
|
||||
>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
|
||||
<Text>{index + 1}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||
<Text>{item.kandang?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>
|
||||
{item.weight_range.weight_min.toFixed(2)} -{' '}
|
||||
{item.weight_range.weight_max.toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatNumber(item.avg_weight_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||
<Text>{formatNumber(item.remaining_chicken_birds)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||
<Text>{formatNumber(item.remaining_chicken_weight_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||
<Text>{formatNumber(item.egg_production_pieces)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||
<Text>{formatNumber(item.egg_production_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{item.feed_suppliers
|
||||
?.map(
|
||||
(s: { alias?: string; name: string }) =>
|
||||
s.alias || s.name
|
||||
)
|
||||
.join(' | ')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>
|
||||
{item.doc_suppliers
|
||||
?.map(
|
||||
(s: { alias?: string; name: string }) =>
|
||||
s.alias || s.name
|
||||
)
|
||||
.join(' | ')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.average_doc_price_rp)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.egg_value_rp)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||
<Text>{formatCurrency(item.hpp_rp)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.remaining_value_rp)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export const generateHppPerKandangPDF = async (
|
||||
data: HppPerKandangExportParams['data'],
|
||||
params: HppPerKandangExportParams['params']
|
||||
): Promise<void> => {
|
||||
const PDFDocument = createPDFDocument(data, params);
|
||||
|
||||
try {
|
||||
const blob = await pdf(PDFDocument).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
const period = params.period || formatDate(new Date(), 'YYYY-MM-DD');
|
||||
link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,959 +0,0 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { ChangeEventHandler } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import Card from '@/components/Card';
|
||||
import SelectInput, {
|
||||
useSelect,
|
||||
OptionType,
|
||||
} from '@/components/input/SelectInput';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import { AreaApi } from '@/services/api/master-data';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
import { KandangApi } from '@/services/api/master-data';
|
||||
import { SaleReportApi } from '@/services/api/report/marketing-sale';
|
||||
import Table from '@/components/Table';
|
||||
import { ColumnDef, Row, flexRender } from '@tanstack/react-table';
|
||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
HppPerKandangReport,
|
||||
HppPerKandangRow,
|
||||
HppPerKandangPerWeightRange,
|
||||
} from '@/types/api/report/hpp-per-kandang';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import { generateHppPerKandangPDF } from '../export/HppPerkandangExport';
|
||||
import toast from 'react-hot-toast';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
const HppPerKandangTab = () => {
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||
|
||||
// ===== SUBMISSION STATE =====
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// ===== TABLE FILTER STATE =====
|
||||
const { state: tableFilterState, updateFilter } = useTableFilter({
|
||||
initial: {
|
||||
area_id: [] as string[],
|
||||
location_id: [] as string[],
|
||||
kandang_id: [] as string[],
|
||||
weight_min: '',
|
||||
weight_max: '',
|
||||
period: '',
|
||||
sort_by: '',
|
||||
show_unrecorded: false,
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
},
|
||||
});
|
||||
|
||||
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
||||
AreaApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
|
||||
useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
|
||||
useSelect(KandangApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const showUnrecordedOptions: OptionType[] = [
|
||||
{ value: 'false', label: 'Sembunyikan' },
|
||||
{ value: 'true', label: 'Tampilkan' },
|
||||
];
|
||||
|
||||
const areaChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||
updateFilter(
|
||||
'area_id',
|
||||
arr.map((v) => String((v as OptionType).value))
|
||||
);
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const locationChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||
updateFilter(
|
||||
'location_id',
|
||||
arr.map((v) => String((v as OptionType).value))
|
||||
);
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const kandangChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||
updateFilter(
|
||||
'kandang_id',
|
||||
arr.map((v) => String((v as OptionType).value))
|
||||
);
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const weightMinChangeHandler = useCallback<
|
||||
ChangeEventHandler<HTMLInputElement>
|
||||
>(
|
||||
(e) => {
|
||||
const val = e.target.value;
|
||||
updateFilter('weight_min', val ? String(parseFloat(val) || 0) : '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const weightMaxChangeHandler = useCallback<
|
||||
ChangeEventHandler<HTMLInputElement>
|
||||
>(
|
||||
(e) => {
|
||||
const val = e.target.value;
|
||||
updateFilter('weight_max', val ? String(parseFloat(val) || 0) : '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
(e) => {
|
||||
const val = e.target.value;
|
||||
updateFilter('period', val || '');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const showUnrecordedChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const newVal = val as OptionType;
|
||||
updateFilter('show_unrecorded', newVal?.value === 'true');
|
||||
setIsSubmitted(false);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const resetFilters = useCallback(() => {
|
||||
updateFilter('area_id', []);
|
||||
updateFilter('location_id', []);
|
||||
updateFilter('kandang_id', []);
|
||||
updateFilter('weight_min', '');
|
||||
updateFilter('weight_max', '');
|
||||
updateFilter('period', '');
|
||||
updateFilter('sort_by', '');
|
||||
updateFilter('show_unrecorded', false);
|
||||
setIsSubmitted(false);
|
||||
}, [updateFilter]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!tableFilterState.period) {
|
||||
toast.error('Periode wajib diisi');
|
||||
return;
|
||||
}
|
||||
setIsSubmitted(true);
|
||||
}, [tableFilterState.period]);
|
||||
|
||||
// ===== DATA FETCHING =====
|
||||
const { data: hppPerKandang, isLoading } = useSWR(
|
||||
isSubmitted
|
||||
? () => {
|
||||
const params = {
|
||||
area_id:
|
||||
tableFilterState.area_id.length > 0
|
||||
? tableFilterState.area_id.join(',')
|
||||
: undefined,
|
||||
location_id:
|
||||
tableFilterState.location_id.length > 0
|
||||
? tableFilterState.location_id.join(',')
|
||||
: undefined,
|
||||
kandang_id:
|
||||
tableFilterState.kandang_id.length > 0
|
||||
? tableFilterState.kandang_id.join(',')
|
||||
: undefined,
|
||||
weight_min: tableFilterState.weight_min || undefined,
|
||||
weight_max: tableFilterState.weight_max || undefined,
|
||||
period: tableFilterState.period || undefined,
|
||||
sort_by: tableFilterState.sort_by || undefined,
|
||||
show_unrecorded: tableFilterState.show_unrecorded,
|
||||
};
|
||||
|
||||
return ['hpp-per-kandang-report', params];
|
||||
}
|
||||
: null,
|
||||
([, params]) =>
|
||||
SaleReportApi.getHppPerKandangReport(
|
||||
params.area_id,
|
||||
params.location_id,
|
||||
params.kandang_id,
|
||||
params.weight_min,
|
||||
params.weight_max,
|
||||
params.period,
|
||||
params.sort_by,
|
||||
params.show_unrecorded
|
||||
)
|
||||
);
|
||||
|
||||
const data: HppPerKandangReport['rows'] = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(hppPerKandang)
|
||||
? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || []
|
||||
: [],
|
||||
[hppPerKandang]
|
||||
);
|
||||
|
||||
const summaryTotal =
|
||||
isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary?.total
|
||||
? hppPerKandang.data.summary.total
|
||||
: undefined;
|
||||
|
||||
const perWeightRangeSummary = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(hppPerKandang) &&
|
||||
hppPerKandang?.data?.summary?.per_weight_range
|
||||
? hppPerKandang.data.summary.per_weight_range
|
||||
: [],
|
||||
[hppPerKandang]
|
||||
);
|
||||
|
||||
const period =
|
||||
isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period
|
||||
? hppPerKandang.data.period
|
||||
: undefined;
|
||||
|
||||
// ===== EXPORT DATA FETCHER =====
|
||||
const hppPerKandangExport =
|
||||
useCallback(async (): Promise<HppPerKandangReport | null> => {
|
||||
const params = {
|
||||
area_id:
|
||||
tableFilterState.area_id.length > 0
|
||||
? tableFilterState.area_id.join(',')
|
||||
: undefined,
|
||||
location_id:
|
||||
tableFilterState.location_id.length > 0
|
||||
? tableFilterState.location_id.join(',')
|
||||
: undefined,
|
||||
kandang_id:
|
||||
tableFilterState.kandang_id.length > 0
|
||||
? tableFilterState.kandang_id.join(',')
|
||||
: undefined,
|
||||
weight_min: tableFilterState.weight_min || undefined,
|
||||
weight_max: tableFilterState.weight_max || undefined,
|
||||
period: tableFilterState.period || undefined,
|
||||
sort_by: tableFilterState.sort_by || undefined,
|
||||
show_unrecorded: tableFilterState.show_unrecorded,
|
||||
limit: 10000,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
const response = await SaleReportApi.getHppPerKandangReport(
|
||||
params.area_id,
|
||||
params.location_id,
|
||||
params.kandang_id,
|
||||
params.weight_min,
|
||||
params.weight_max,
|
||||
params.period,
|
||||
params.sort_by,
|
||||
params.show_unrecorded
|
||||
);
|
||||
|
||||
return isResponseSuccess(response) ? response.data : null;
|
||||
}, [tableFilterState]);
|
||||
|
||||
// ===== TABLE COLUMNS DEFINITION =====
|
||||
const allFeedSuppliers = useMemo(() => {
|
||||
const suppliers = new Set<string>();
|
||||
data.forEach((item: HppPerKandangRow) => {
|
||||
item.feed_suppliers?.forEach((s: { alias?: string; name: string }) => {
|
||||
suppliers.add(s.alias || s.name);
|
||||
});
|
||||
});
|
||||
return Array.from(suppliers).join(' | ');
|
||||
}, [data]);
|
||||
|
||||
const allDocSuppliers = useMemo(() => {
|
||||
const suppliers = new Set<string>();
|
||||
data.forEach((item: HppPerKandangRow) => {
|
||||
item.doc_suppliers?.forEach((s: { alias?: string; name: string }) => {
|
||||
suppliers.add(s.alias || s.name);
|
||||
});
|
||||
});
|
||||
return Array.from(suppliers).join(' | ');
|
||||
}, [data]);
|
||||
|
||||
// ===== EXPORT HANDLERS =====
|
||||
const handleExportExcel = useCallback(async () => {
|
||||
setIsExcelExportLoading(true);
|
||||
try {
|
||||
const allDataForExport = await hppPerKandangExport();
|
||||
|
||||
if (
|
||||
!allDataForExport ||
|
||||
!allDataForExport?.rows ||
|
||||
allDataForExport.rows.length === 0
|
||||
) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
|
||||
const allExportData =
|
||||
allDataForExport.rows as HppPerKandangReport['rows'];
|
||||
|
||||
const summaryTotal = allDataForExport.summary.total;
|
||||
|
||||
const excelData: { [key: string]: string | number }[] = allExportData.map(
|
||||
(item: HppPerKandangRow, index: number) => ({
|
||||
No: index + 1,
|
||||
Kandang: item.kandang?.name || '',
|
||||
'Rentang Bobot': item.weight_range
|
||||
? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}`
|
||||
: '',
|
||||
'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0,
|
||||
'Sisa Ayam (Ekor)': item.remaining_chicken_birds || 0,
|
||||
'Sisa Ayam (KG)': item.remaining_chicken_weight_kg || 0,
|
||||
'Produksi Telur (Butir)': item.egg_production_pieces || 0,
|
||||
'Produksi Telur (KG)': item.egg_production_kg || 0,
|
||||
'Feed (Supplier)':
|
||||
item.feed_suppliers
|
||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
||||
.join(' | ') || '',
|
||||
'DOC (Supplier)':
|
||||
item.doc_suppliers
|
||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
||||
.join(' | ') || '',
|
||||
'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0,
|
||||
'Nilai Nominal Telur (RP)': item.egg_value_rp || 0,
|
||||
'HPP Ayam (RP)': item.hpp_rp || 0,
|
||||
'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0,
|
||||
'Nilai Nominal Sisa Ayam (RP)': item.remaining_value_rp || 0,
|
||||
})
|
||||
);
|
||||
|
||||
excelData.push({
|
||||
No: 'TOTAL',
|
||||
Kandang: 'ALL',
|
||||
'Rentang Bobot': '-',
|
||||
'Rata-Rata Bobot (KG)': summaryTotal?.average_weight_kg || 0,
|
||||
'Sisa Ayam (Ekor)': summaryTotal?.total_remaining_chicken_birds || 0,
|
||||
'Sisa Ayam (KG)': summaryTotal?.total_remaining_chicken_weight_kg || 0,
|
||||
'Produksi Telur (Butir)':
|
||||
summaryTotal?.total_egg_production_pieces || 0,
|
||||
'Produksi Telur (KG)': summaryTotal?.total_egg_production_kg || 0,
|
||||
'Feed (Supplier)': allFeedSuppliers,
|
||||
'DOC (Supplier)': allDocSuppliers,
|
||||
'Rata-Rata Harga DOC (RP)':
|
||||
summaryTotal?.total_average_doc_price_rp || 0,
|
||||
'Nilai Nominal Telur (RP)': summaryTotal?.total_egg_value_rp || 0,
|
||||
'HPP Ayam (RP)': summaryTotal?.total_hpp_rp || 0,
|
||||
'HPP Telur (RP/KG)': summaryTotal?.average_egg_hpp_rp_per_kg || 0,
|
||||
'Nilai Nominal Sisa Ayam (RP)':
|
||||
summaryTotal?.total_remaining_value_rp || 0,
|
||||
});
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
||||
|
||||
const colWidths = [
|
||||
{ wch: 5 }, // No
|
||||
{ wch: 30 }, // Kandang
|
||||
{ wch: 15 }, // Rentang Bobot
|
||||
{ wch: 18 }, // Rata-Rata Bobot (KG)
|
||||
{ wch: 15 }, // Sisa Ayam (Ekor)
|
||||
{ wch: 15 }, // Sisa Ayam (KG)
|
||||
{ wch: 18 }, // Produksi Telur (Butir)
|
||||
{ wch: 18 }, // Produksi Telur (KG)
|
||||
{ wch: 20 }, // Feed (Supplier)
|
||||
{ wch: 20 }, // DOC (Supplier)
|
||||
{ wch: 20 }, // Rata-Rata Harga DOC (RP)
|
||||
{ wch: 20 }, // Nilai Nominal Telur (RP)
|
||||
{ wch: 15 }, // HPP Ayam (RP)
|
||||
{ wch: 18 }, // HPP Telur (RP/KG)
|
||||
{ wch: 25 }, // Nilai Nominal Sisa Ayam (RP)
|
||||
];
|
||||
worksheet['!cols'] = colWidths;
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang');
|
||||
|
||||
const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`;
|
||||
|
||||
XLSX.writeFile(workbook, filename);
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
hppPerKandangExport,
|
||||
tableFilterState,
|
||||
areaOptions,
|
||||
locationOptions,
|
||||
kandangOptions,
|
||||
]);
|
||||
|
||||
const handleExportPDF = useCallback(async () => {
|
||||
setIsPdfExportLoading(true);
|
||||
try {
|
||||
const allDataForExport = await hppPerKandangExport();
|
||||
|
||||
if (
|
||||
!allDataForExport ||
|
||||
!allDataForExport?.rows ||
|
||||
allDataForExport.rows.length === 0
|
||||
) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
|
||||
const areaName =
|
||||
tableFilterState.area_id.length > 0
|
||||
? tableFilterState.area_id
|
||||
.map(
|
||||
(id) =>
|
||||
areaOptions.find((opt) => opt.value === Number(id))?.label
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(', ') || 'Semua Area'
|
||||
: 'Semua Area';
|
||||
|
||||
const locationName =
|
||||
tableFilterState.location_id.length > 0
|
||||
? tableFilterState.location_id
|
||||
.map(
|
||||
(id) =>
|
||||
locationOptions.find((opt) => opt.value === Number(id))?.label
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(', ') || 'Semua Lokasi'
|
||||
: 'Semua Lokasi';
|
||||
|
||||
const kandangName =
|
||||
tableFilterState.kandang_id.length > 0
|
||||
? tableFilterState.kandang_id
|
||||
.map(
|
||||
(id) =>
|
||||
kandangOptions.find((opt) => opt.value === Number(id))?.label
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(', ') || 'Semua Kandang'
|
||||
: 'Semua Kandang';
|
||||
|
||||
await generateHppPerKandangPDF(allDataForExport, {
|
||||
area_name: areaName,
|
||||
location_name: locationName,
|
||||
kandang_name: kandangName,
|
||||
period: tableFilterState.period,
|
||||
weight_min: tableFilterState.weight_min,
|
||||
weight_max: tableFilterState.weight_max,
|
||||
show_unrecorded: tableFilterState.show_unrecorded.toString(),
|
||||
sort_by: tableFilterState.sort_by,
|
||||
});
|
||||
|
||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsPdfExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
hppPerKandangExport,
|
||||
tableFilterState,
|
||||
areaOptions,
|
||||
locationOptions,
|
||||
kandangOptions,
|
||||
]);
|
||||
|
||||
const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => {
|
||||
const tableColumns: ColumnDef<HppPerKandangReport['rows'][0]>[] = [
|
||||
{
|
||||
id: 'no',
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
footer: () => <div className='font-semibold text-gray-900'>TOTAL</div>,
|
||||
},
|
||||
{
|
||||
id: 'kandang_name',
|
||||
header: 'Kandang',
|
||||
accessorKey: 'kandang.name',
|
||||
cell: (props) => {
|
||||
const kandang = props.row.original.kandang;
|
||||
return kandang?.name || '-';
|
||||
},
|
||||
footer: () => <div className='font-semibold text-gray-900'>ALL</div>,
|
||||
},
|
||||
{
|
||||
id: 'weight_range',
|
||||
header: 'Rentang Bobot',
|
||||
accessorKey: 'weight_range',
|
||||
cell: (props) => {
|
||||
const weightRange = props.row.original.weight_range;
|
||||
return weightRange
|
||||
? `${formatNumber(weightRange.weight_min)} - ${formatNumber(weightRange.weight_max)}`
|
||||
: '-';
|
||||
},
|
||||
footer: () => <div className='font-semibold text-gray-900'>-</div>,
|
||||
},
|
||||
{
|
||||
id: 'avg_weight_kg',
|
||||
header: 'Rata-Rata Bobot (KG)',
|
||||
accessorKey: 'avg_weight_kg',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.avg_weight_kg;
|
||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatNumber(summaryTotal?.average_weight_kg || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'remaining_chicken_birds',
|
||||
header: 'Sisa Ayam (Ekor)',
|
||||
accessorKey: 'remaining_chicken_birds',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.remaining_chicken_birds;
|
||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatNumber(summaryTotal?.total_remaining_chicken_birds || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'remaining_chicken_weight_kg',
|
||||
header: 'Sisa Ayam (KG)',
|
||||
accessorKey: 'remaining_chicken_weight_kg',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.remaining_chicken_weight_kg;
|
||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'egg_production_pieces',
|
||||
header: 'Produksi Telur (Butir)',
|
||||
accessorKey: 'egg_production_pieces',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.egg_production_pieces;
|
||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatNumber(summaryTotal?.total_egg_production_pieces || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'egg_production_kg',
|
||||
header: 'Produksi Telur (KG)',
|
||||
accessorKey: 'egg_production_kg',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.egg_production_kg;
|
||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'feed_suppliers',
|
||||
header: 'Feed (Supplier)',
|
||||
accessorKey: 'feed_suppliers',
|
||||
cell: (props) => {
|
||||
const suppliers = props.row.original.feed_suppliers;
|
||||
return (
|
||||
suppliers
|
||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
||||
.join(' | ') || '-'
|
||||
);
|
||||
},
|
||||
footer: () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{allFeedSuppliers || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'doc_suppliers',
|
||||
header: 'DOC (Supplier)',
|
||||
accessorKey: 'doc_suppliers',
|
||||
cell: (props) => {
|
||||
const suppliers = props.row.original.doc_suppliers;
|
||||
return (
|
||||
suppliers
|
||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
||||
.join(' | ') || '-'
|
||||
);
|
||||
},
|
||||
footer: () => (
|
||||
<div className='font-semibold text-gray-900'>
|
||||
{allDocSuppliers || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'average_doc_price_rp',
|
||||
header: 'Rata-Rata Harga DOC (RP)',
|
||||
accessorKey: 'average_doc_price_rp',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.average_doc_price_rp;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summaryTotal?.total_average_doc_price_rp || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'egg_value_rp',
|
||||
header: 'Nilai Nominal Telur (RP)',
|
||||
accessorKey: 'egg_value_rp',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.egg_value_rp;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summaryTotal?.total_egg_value_rp || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'hpp_rp',
|
||||
header: 'HPP Ayam (RP)',
|
||||
accessorKey: 'hpp_rp',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.hpp_rp;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summaryTotal?.total_hpp_rp || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'egg_hpp_rp_per_kg',
|
||||
header: 'HPP Telur (RP/KG)',
|
||||
accessorKey: 'egg_hpp_rp_per_kg',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.egg_hpp_rp_per_kg;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'remaining_value_rp',
|
||||
header: 'Nilai Nominal Sisa Ayam (RP)',
|
||||
accessorKey: 'remaining_value_rp',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.remaining_value_rp;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{formatCurrency(summaryTotal?.total_remaining_value_rp || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
return tableColumns;
|
||||
};
|
||||
|
||||
// ===== CUSTOM ROW RENDERER =====
|
||||
const renderCustomRow = useCallback(
|
||||
(row: Row<HppPerKandangReport['rows'][0]>) => {
|
||||
if (row.index === data.length - 1) {
|
||||
const defaultRow = (
|
||||
<tr
|
||||
key={row.id}
|
||||
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border-gray-200'
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
|
||||
const customRows = [
|
||||
<tr
|
||||
className='border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||
key={'rekapitulasi-row'}
|
||||
>
|
||||
<td
|
||||
colSpan={15}
|
||||
className='px-4 py-3 text-gray-900 text-center font-semibold'
|
||||
>
|
||||
Rekapitulasi per rentang bobot
|
||||
</td>
|
||||
</tr>,
|
||||
];
|
||||
|
||||
if (perWeightRangeSummary.length > 0) {
|
||||
perWeightRangeSummary.forEach(
|
||||
(item: HppPerKandangPerWeightRange, index = 0) => {
|
||||
customRows.push(
|
||||
<tr
|
||||
key={`summary-${item.id}`}
|
||||
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200 [&_td]:px-4 [&_td]:py-3 [&_td]:text-xs [&_td]:text-gray-900 [&_td]:whitespace-nowrap'
|
||||
>
|
||||
<td className=''>{index + 1}</td>
|
||||
<td className=''>ALL</td>
|
||||
<td className=''>{item.label}</td>
|
||||
<td className='text-right'>
|
||||
{formatNumber(item.avg_weight_kg)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatNumber(item.remaining_chicken_birds)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatNumber(item.remaining_chicken_weight_kg)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatNumber(item.egg_production_pieces)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatNumber(item.egg_production_kg)}
|
||||
</td>
|
||||
<td className=''>
|
||||
{item.feed_suppliers
|
||||
?.map((s) => s.alias || s.name)
|
||||
.join(' | ') || '-'}
|
||||
</td>
|
||||
<td className=''>
|
||||
{item.doc_suppliers
|
||||
?.map((s) => s.alias || s.name)
|
||||
.join(' | ') || '-'}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatCurrency(item.average_doc_price_rp)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatCurrency(item.egg_value_rp)}
|
||||
</td>
|
||||
<td className='text-right'>{formatCurrency(item.hpp_rp)}</td>
|
||||
<td className='text-right'>
|
||||
{formatCurrency(item.egg_hpp_rp_per_kg)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatCurrency(item.remaining_value_rp)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return [defaultRow, ...customRows];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[data, perWeightRangeSummary]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='w-full p-0 sm:p-4'>
|
||||
<Card
|
||||
subtitle={
|
||||
period
|
||||
? `Laporan > HPP Harian Kandang (${period})`
|
||||
: 'Laporan > HPP Harian Kandang'
|
||||
}
|
||||
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
||||
>
|
||||
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||
<SelectInput
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
isMulti
|
||||
options={areaOptions}
|
||||
value={areaOptions.filter((opt) =>
|
||||
(tableFilterState.area_id || [])
|
||||
.map(String)
|
||||
.includes(String(opt.value))
|
||||
)}
|
||||
onChange={areaChangeHandler}
|
||||
isLoading={isLoadingAreas}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
placeholder='Pilih Lokasi'
|
||||
isMulti
|
||||
options={locationOptions}
|
||||
value={locationOptions.filter((opt) =>
|
||||
(tableFilterState.location_id || [])
|
||||
.map(String)
|
||||
.includes(String(opt.value))
|
||||
)}
|
||||
onChange={locationChangeHandler}
|
||||
isLoading={isLoadingLocations}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
label='Kandang'
|
||||
placeholder='Pilih Kandang'
|
||||
isMulti
|
||||
options={kandangOptions}
|
||||
value={kandangOptions.filter((opt) =>
|
||||
(tableFilterState.kandang_id || [])
|
||||
.map(String)
|
||||
.includes(String(opt.value))
|
||||
)}
|
||||
onChange={kandangChangeHandler}
|
||||
isLoading={isLoadingKandangs}
|
||||
isClearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||
<div className='flex flex-row gap-4'>
|
||||
<NumberInput
|
||||
label='Rentang Bobot Min (Kg)'
|
||||
name='weight_min'
|
||||
placeholder='Masukkan bobot minimum'
|
||||
value={tableFilterState.weight_min}
|
||||
onChange={weightMinChangeHandler}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Rentang Bobot Max (Kg)'
|
||||
name='weight_max'
|
||||
placeholder='Masukkan bobot maximum'
|
||||
value={tableFilterState.weight_max}
|
||||
onChange={weightMaxChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
<DateInput
|
||||
label='Periode'
|
||||
name='period'
|
||||
placeholder='Pilih Periode'
|
||||
value={tableFilterState.period}
|
||||
onChange={periodChangeHandler}
|
||||
required
|
||||
/>
|
||||
<SelectInput
|
||||
label='Tampilkan Kandang Tanpa Recording'
|
||||
placeholder='Pilih Opsi'
|
||||
options={showUnrecordedOptions}
|
||||
value={
|
||||
tableFilterState.show_unrecorded
|
||||
? showUnrecordedOptions.find((opt) => opt.value === 'true') ||
|
||||
null
|
||||
: showUnrecordedOptions.find((opt) => opt.value === 'false') ||
|
||||
null
|
||||
}
|
||||
onChange={showUnrecordedChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||
<Button color='primary' onClick={handleSubmit}>
|
||||
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||
Cari
|
||||
</Button>
|
||||
<Button color='warning' onClick={resetFilters}>
|
||||
<Icon icon='heroicons-outline:refresh' width={20} height={20} />
|
||||
Reset
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger={
|
||||
<Button color='success' isLoading={isAnyExportLoading}>
|
||||
Export
|
||||
<Icon
|
||||
icon='heroicons-outline:download'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
align='end'
|
||||
>
|
||||
<Menu className='w-32'>
|
||||
<MenuItem title='Excel' onClick={handleExportExcel} />
|
||||
<MenuItem title='PDF' onClick={handleExportPDF} />
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div className='divider'></div>
|
||||
|
||||
{!isSubmitted ? (
|
||||
<div className='mt-6 text-center text-gray-500'>
|
||||
Silakan pilih filter dan klik tombol Cari untuk menampilkan data.
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
) : data.length === 0 ? (
|
||||
<div className='mt-6 text-center text-gray-500'>
|
||||
Tidak ada data yang dapat ditampilkan...
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
data={data}
|
||||
columns={getTableColumns()}
|
||||
renderFooter={data.length > 0}
|
||||
renderCustomRow={renderCustomRow}
|
||||
className={{
|
||||
containerClassName: 'w-full mt-6',
|
||||
tableWrapperClassName: 'overflow-x-auto mt-4',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HppPerKandangTab;
|
||||
@@ -9,28 +9,9 @@ export const PROJECT_FLOCK_APPROVAL_LINE: ApprovalLine = [
|
||||
step_number: 2,
|
||||
step_name: 'Aktif',
|
||||
},
|
||||
{
|
||||
step_number: 3,
|
||||
step_name: 'Selesai',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const PROJECT_FLOCK_KANDANGS_APPROVAL_LINE: ApprovalLine = [
|
||||
// {
|
||||
// step_number: 1,
|
||||
// step_name: 'Pengajuan',
|
||||
// },
|
||||
{
|
||||
step_number: 2,
|
||||
step_name: 'Disetujui',
|
||||
},
|
||||
{
|
||||
step_number: 3,
|
||||
step_name: 'Selesai',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const CHICKINS_APPROVAL_LINE: ApprovalLine = [
|
||||
export const PROJECT_FLOCK_KANDANG_APPROVAL_LINE: ApprovalLine = [
|
||||
{
|
||||
step_number: 1,
|
||||
step_name: 'Pengajuan',
|
||||
|
||||
+14
-88
@@ -10,20 +10,14 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
text: 'Produksi',
|
||||
link: '/production',
|
||||
icon: 'heroicons-outline:wrench-screwdriver',
|
||||
permission: [
|
||||
'lti.production.project_flocks.list',
|
||||
'lti.production.recording.list',
|
||||
],
|
||||
submenu: [
|
||||
{
|
||||
text: 'Daftar Flock',
|
||||
link: '/production/project-flock',
|
||||
permission: ['lti.production.project_flocks.list'],
|
||||
},
|
||||
{
|
||||
text: 'Recording',
|
||||
link: '/production/recording',
|
||||
permission: ['lti.production.recording.list'],
|
||||
},
|
||||
{
|
||||
text: 'Transfer to Laying',
|
||||
@@ -35,7 +29,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
text: 'Pembelian',
|
||||
link: '/purchase',
|
||||
icon: 'heroicons-outline:shopping-cart',
|
||||
permission: ['lti.purchase.list'],
|
||||
},
|
||||
{
|
||||
text: 'Penjualan',
|
||||
@@ -43,60 +36,36 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
icon: 'heroicons-outline:currency-dollar',
|
||||
},
|
||||
{
|
||||
text: 'Biaya Operasional',
|
||||
text: 'Biaya',
|
||||
link: '/expense',
|
||||
icon: 'heroicons:wallet',
|
||||
permission: ['lti.expense.list'],
|
||||
},
|
||||
{
|
||||
text: 'Keuangan',
|
||||
link: '/finance',
|
||||
icon: 'heroicons:wallet-solid',
|
||||
},
|
||||
{
|
||||
text: 'Closing',
|
||||
link: '/closing',
|
||||
icon: 'heroicons-outline:presentation-chart-bar',
|
||||
permission: ['lti.closing.list'],
|
||||
},
|
||||
{
|
||||
text: 'Laporan',
|
||||
link: '/report',
|
||||
icon: 'mdi:chart-box-outline',
|
||||
submenu: [
|
||||
{
|
||||
text: 'Logistik & Persediaan',
|
||||
link: '/report/logistic-stock',
|
||||
},
|
||||
{
|
||||
text: 'Biaya Operasional',
|
||||
link: '/report/expense',
|
||||
},
|
||||
{
|
||||
text: 'Penjualan',
|
||||
link: '/report/marketing',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Persediaan',
|
||||
link: '/inventory',
|
||||
icon: 'heroicons-outline:folder',
|
||||
permission: [
|
||||
'lti.inventory.product_stock.list',
|
||||
'lti.inventory.product_warehouses.list',
|
||||
'lti.inventory.transfer.list',
|
||||
],
|
||||
submenu: [
|
||||
{
|
||||
text: 'Stok Produk',
|
||||
text: 'Produk',
|
||||
link: '/inventory/product',
|
||||
permission: ['lti.inventory.product_stock.list'],
|
||||
},
|
||||
{
|
||||
text: 'Penyesuaian Stok',
|
||||
link: '/inventory/adjustment',
|
||||
permission: ['lti.inventory.product_stock.list'],
|
||||
},
|
||||
{
|
||||
text: 'Transfer Stok',
|
||||
link: '/inventory/movement',
|
||||
permission: ['lti.inventory.transfer.list'],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -104,86 +73,58 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
text: 'Master Data',
|
||||
link: '/master-data',
|
||||
icon: 'heroicons-outline:circle-stack',
|
||||
permission: [
|
||||
'lti.master.area.list',
|
||||
'lti.master.banks.list',
|
||||
'lti.master.customer.list',
|
||||
'lti.master.fcr.list',
|
||||
'lti.master.flocks.list',
|
||||
'lti.master.kandangs.list',
|
||||
'lti.master.locations.list',
|
||||
'lti.master.nonstocks.list',
|
||||
'lti.master.product_categories.list',
|
||||
'lti.master.products.list',
|
||||
'lti.master.suppliers.list',
|
||||
'lti.master.uoms.list',
|
||||
'lti.master.warehouses.list',
|
||||
],
|
||||
submenu: [
|
||||
{
|
||||
text: 'Produk',
|
||||
link: '/master-data/product',
|
||||
permission: ['lti.master.products.list'],
|
||||
},
|
||||
{
|
||||
text: 'Kategori Produk',
|
||||
link: '/master-data/product-category',
|
||||
permission: ['lti.master.product_categories.list'],
|
||||
},
|
||||
{
|
||||
text: 'Bank',
|
||||
link: '/master-data/bank',
|
||||
permission: ['lti.master.banks.list'],
|
||||
},
|
||||
{
|
||||
text: 'Area',
|
||||
link: '/master-data/area',
|
||||
permission: ['lti.master.area.list'],
|
||||
},
|
||||
{
|
||||
text: 'Lokasi',
|
||||
link: '/master-data/location',
|
||||
permission: ['lti.master.locations.list'],
|
||||
},
|
||||
{
|
||||
text: 'Kandang',
|
||||
link: '/master-data/kandang',
|
||||
permission: ['lti.master.kandangs.list'],
|
||||
},
|
||||
{
|
||||
text: 'Warehouse',
|
||||
link: '/master-data/warehouse',
|
||||
permission: ['lti.master.warehouses.list'],
|
||||
},
|
||||
{
|
||||
text: 'Customer',
|
||||
link: '/master-data/customer',
|
||||
permission: ['lti.master.customer.list'],
|
||||
},
|
||||
{
|
||||
text: 'UOM',
|
||||
link: '/master-data/uom',
|
||||
permission: ['lti.master.uoms.list'],
|
||||
},
|
||||
{
|
||||
text: 'Non-Stock',
|
||||
link: '/master-data/nonstock',
|
||||
permission: ['lti.master.nonstocks.list'],
|
||||
},
|
||||
{
|
||||
text: 'FCR',
|
||||
link: '/master-data/fcr',
|
||||
permission: ['lti.master.fcr.list'],
|
||||
},
|
||||
{
|
||||
text: 'Supplier',
|
||||
link: '/master-data/supplier',
|
||||
permission: ['lti.master.suppliers.list'],
|
||||
},
|
||||
{
|
||||
text: 'Flock',
|
||||
link: '/master-data/flock',
|
||||
permission: ['lti.master.flocks.list'],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -316,28 +257,13 @@ export const ACCEPTED_FILE_TYPE = {
|
||||
},
|
||||
};
|
||||
|
||||
export const FILTER_TYPE_OPTIONS = [
|
||||
{
|
||||
label: 'Tanggal Realisasi',
|
||||
value: 'REALIZATION_DATE',
|
||||
},
|
||||
{
|
||||
label: 'Tanggal DO',
|
||||
value: 'DO_DATE',
|
||||
},
|
||||
export const FINANCE_TRANSACTION_TYPE_OPTIONS = [
|
||||
{ value: 'REVENUE', label: 'Pemasukan' },
|
||||
{ value: 'EXPENSE', label: 'Pengeluaran' },
|
||||
];
|
||||
|
||||
export const MARKETING_TYPE_OPTIONS = [
|
||||
{
|
||||
label: 'Ayam',
|
||||
value: 'ayam',
|
||||
},
|
||||
{
|
||||
label: 'Telur',
|
||||
value: 'telur',
|
||||
},
|
||||
{
|
||||
label: 'Trading',
|
||||
value: 'trading',
|
||||
},
|
||||
export const FINANCE_PAYMENT_METHOD_OPTIONS = [
|
||||
{ value: 'TRANSFER', label: 'Transfer' },
|
||||
{ value: 'CASH', label: 'Cash' },
|
||||
{ value: 'GIRO', label: 'Giro' },
|
||||
];
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
'/': ['lti.dashboard.list'],
|
||||
|
||||
// Dashboard
|
||||
'/dashboard/': ['lti.dashboard.list'],
|
||||
|
||||
// Production
|
||||
// Production - Project Flock
|
||||
'/production/project-flock/': ['lti.production.project_flocks.list'],
|
||||
'/production/project-flock/add/': ['lti.production.project_flocks.create'],
|
||||
'/production/project-flock/detail/': ['lti.production.project_flocks.detail'],
|
||||
'/production/project-flock/detail/edit/': [
|
||||
'lti.production.project_flocks.update',
|
||||
],
|
||||
'/production/project-flock/chickin/add/kandang/': [
|
||||
'lti.production.chickins.create',
|
||||
],
|
||||
'/production/project-flock/closing/': [
|
||||
'lti.production.project_flock_kandangs.closing',
|
||||
],
|
||||
|
||||
// Production - Recording
|
||||
'/production/recording/': ['lti.production.recording.list'],
|
||||
'/production/recording/add/': ['lti.production.recording.create'],
|
||||
'/production/recording/detail/': ['lti.production.recording.detail'],
|
||||
'/production/recording/detail/edit/': ['lti.production.recording.update'],
|
||||
|
||||
// Production - Transfer to Laying
|
||||
'/production/transfer-to-laying/': ['lti.production.transfer_to_laying.list'],
|
||||
'/production/transfer-to-laying/add/': [
|
||||
'lti.production.transfer_to_laying.create',
|
||||
],
|
||||
'/production/transfer-to-laying/detail/': [
|
||||
'lti.production.transfer_to_laying.detail',
|
||||
],
|
||||
'/production/transfer-to-laying/detail/edit/': [
|
||||
'lti.production.transfer_to_laying.update',
|
||||
],
|
||||
|
||||
// Purchase
|
||||
'/purchase/': ['lti.purchase.list'],
|
||||
'/purchase/add/': ['lti.purchase.create'],
|
||||
'/purchase/detail/': ['lti.purchase.detail'],
|
||||
'/purchase/detail/edit/': ['lti.purchase.update'],
|
||||
|
||||
// Marketing
|
||||
'/marketing/': ['lti.marketing.delivery_order.list'],
|
||||
'/marketing/add/delivery-orders/': ['lti.marketing.delivery_order.create'],
|
||||
'/marketing/add/sales-orders/': ['lti.marketing.sales_order.create'],
|
||||
'/marketing/detail/': ['lti.marketing.detail'],
|
||||
'/marketing/detail/delivery-orders/edit/': [
|
||||
'lti.marketing.delivery_order.update',
|
||||
],
|
||||
'/marketing/detail/sales-orders/edit/': ['lti.marketing.sales_order.update'],
|
||||
|
||||
// Expense
|
||||
'/expense/': ['lti.expense.list'],
|
||||
'/expense/add/': ['lti.expense.create'],
|
||||
'/expense/detail/': ['lti.expense.detail'],
|
||||
'/expense/detail/edit/': ['lti.expense.update'],
|
||||
'/expense/realization/': ['lti.expense.create.realization'],
|
||||
'/expense/realization/edit/': ['lti.expense.update.realization'],
|
||||
|
||||
// Closing
|
||||
'/closing/': ['lti.closing.list'],
|
||||
'/closing/detail/': ['lti.closing.detail'],
|
||||
|
||||
// Report
|
||||
'/report/logistic-stock/': ['lti.report.purchasesupplier.list'],
|
||||
'/report/expense/': ['lti.report.expense.list'],
|
||||
'/report/marketing/': ['lti.report.expense.list'],
|
||||
|
||||
// Inventory
|
||||
'/inventory/adjustment/': ['lti.inventory.list'],
|
||||
'/inventory/adjustment/add/': ['lti.inventory.create'],
|
||||
'/inventory/adjustment/detail/': ['lti.inventory.detail'],
|
||||
'/inventory/movement/': ['lti.inventory.transfer.list'],
|
||||
'/inventory/movement/add/': ['lti.inventory.transfer.create'],
|
||||
'/inventory/movement/detail/': ['lti.inventory.transfer.detail'],
|
||||
'/inventory/movement/detail/edit/': ['lti.inventory.transfer.update'],
|
||||
'/inventory/product/': ['lti.inventory.product_stock.list'],
|
||||
'/inventory/product/detail/': ['lti.inventory.product_stock.detail'],
|
||||
|
||||
// Master Data
|
||||
'/master-data/product/': ['lti.master.products.list'],
|
||||
'/master-data/product/add/': ['lti.master.products.create'],
|
||||
'/master-data/product/detail/': ['lti.master.products.detail'],
|
||||
'/master-data/product/detail/edit/': ['lti.master.products.update'],
|
||||
|
||||
'/master-data/product-category/': ['lti.master.product_categories.list'],
|
||||
'/master-data/product-category/add/': [
|
||||
'lti.master.product_categories.create',
|
||||
],
|
||||
'/master-data/product-category/detail/': [
|
||||
'lti.master.product_categories.detail',
|
||||
],
|
||||
'/master-data/product-category/detail/edit/': [
|
||||
'lti.master.product_categories.update',
|
||||
],
|
||||
|
||||
'/master-data/bank/': ['lti.master.banks.list'],
|
||||
'/master-data/bank/add/': ['lti.master.banks.create'],
|
||||
'/master-data/bank/detail/': ['lti.master.banks.detail'],
|
||||
'/master-data/bank/detail/edit/': ['lti.master.banks.update'],
|
||||
|
||||
'/master-data/area/': ['lti.master.area.list'],
|
||||
'/master-data/area/add/': ['lti.master.area.create'],
|
||||
'/master-data/area/detail/': ['lti.master.area.detail'],
|
||||
'/master-data/area/detail/edit/': ['lti.master.area.update'],
|
||||
|
||||
'/master-data/location/': ['lti.master.locations.list'],
|
||||
'/master-data/location/add/': ['lti.master.locations.create'],
|
||||
'/master-data/location/detail/': ['lti.master.locations.detail'],
|
||||
'/master-data/location/detail/edit/': ['lti.master.locations.update'],
|
||||
|
||||
'/master-data/kandang/': ['lti.master.kandangs.list'],
|
||||
'/master-data/kandang/add/': ['lti.master.kandangs.create'],
|
||||
'/master-data/kandang/detail/': ['lti.master.kandangs.detail'],
|
||||
'/master-data/kandang/detail/edit/': ['lti.master.kandangs.update'],
|
||||
|
||||
'/master-data/warehouse/': ['lti.master.warehouses.list'],
|
||||
'/master-data/warehouse/add/': ['lti.master.warehouses.create'],
|
||||
'/master-data/warehouse/detail/': ['lti.master.warehouses.detail'],
|
||||
'/master-data/warehouse/detail/edit/': ['lti.master.warehouses.update'],
|
||||
|
||||
'/master-data/customer/': ['lti.master.customer.list'],
|
||||
'/master-data/customer/add/': ['lti.master.customer.create'],
|
||||
'/master-data/customer/detail/': ['lti.master.customer.detail'],
|
||||
'/master-data/customer/detail/edit/': ['lti.master.customer.update'],
|
||||
|
||||
'/master-data/uom/': ['lti.master.uoms.list'],
|
||||
'/master-data/uom/add/': ['lti.master.uoms.create'],
|
||||
'/master-data/uom/detail/': ['lti.master.uoms.detail'],
|
||||
'/master-data/uom/detail/edit/': ['lti.master.uoms.update'],
|
||||
|
||||
'/master-data/nonstock/': ['lti.master.nonstocks.list'],
|
||||
'/master-data/nonstock/add/': ['lti.master.nonstocks.create'],
|
||||
'/master-data/nonstock/detail/': ['lti.master.nonstocks.detail'],
|
||||
'/master-data/nonstock/detail/edit/': ['lti.master.nonstocks.update'],
|
||||
|
||||
'/master-data/fcr/': ['lti.master.fcr.list'],
|
||||
'/master-data/fcr/add/': ['lti.master.fcr.create'],
|
||||
'/master-data/fcr/detail/': ['lti.master.fcr.detail'],
|
||||
'/master-data/fcr/detail/edit/': ['lti.master.fcr.update'],
|
||||
|
||||
'/master-data/supplier/': ['lti.master.suppliers.list'],
|
||||
'/master-data/supplier/add/': ['lti.master.suppliers.create'],
|
||||
'/master-data/supplier/detail/': ['lti.master.suppliers.detail'],
|
||||
'/master-data/supplier/detail/edit/': ['lti.master.suppliers.update'],
|
||||
|
||||
'/master-data/flock/': ['lti.master.flocks.list'],
|
||||
'/master-data/flock/add/': ['lti.master.flocks.create'],
|
||||
'/master-data/flock/detail/': ['lti.master.flocks.detail'],
|
||||
'/master-data/flock/detail/edit/': ['lti.master.flocks.update'],
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user