Merge branch 'development' into feat/FE/US-164/TASK-200-204-205-206-207-expense-realization

This commit is contained in:
ValdiANS
2025-11-25 10:55:52 +07:00
86 changed files with 11655 additions and 5334 deletions
-3
View File
@@ -40,8 +40,5 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
# prettier
.prettierrc
# idea
.idea
+519 -19
View File
@@ -8,12 +8,12 @@
"name": "lti-web-client",
"version": "0.1.0",
"dependencies": {
"@react-pdf/renderer": "^4.3.1",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.12.2",
"clsx": "^2.1.1",
"formik": "^2.4.6",
"inputmask": "^5.0.9",
"moment": "^2.30.1",
"next": "15.5.3",
"react": "19.1.0",
@@ -33,7 +33,6 @@
"@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4",
"@types/inputmask": "^5.0.7",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -1274,6 +1273,180 @@
"node": ">=12.4.0"
}
},
"node_modules/@react-pdf/fns": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz",
"integrity": "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==",
"license": "MIT"
},
"node_modules/@react-pdf/font": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.3.tgz",
"integrity": "sha512-N1qQDZr6phXYQOp033Hvm2nkUkx2LkszjGPbmRavs9VOYzi4sp31MaccMKptL24ii6UhBh/z9yPUhnuNe/qHwA==",
"license": "MIT",
"dependencies": {
"@react-pdf/pdfkit": "^4.0.4",
"@react-pdf/types": "^2.9.1",
"fontkit": "^2.0.2",
"is-url": "^1.2.4"
}
},
"node_modules/@react-pdf/image": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.3.tgz",
"integrity": "sha512-lvP5ryzYM3wpbO9bvqLZYwEr5XBDX9jcaRICvtnoRqdJOo7PRrMnmB4MMScyb+Xw10mGeIubZAAomNAG5ONQZQ==",
"license": "MIT",
"dependencies": {
"@react-pdf/png-js": "^3.0.0",
"jay-peg": "^1.1.1"
}
},
"node_modules/@react-pdf/layout": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.4.1.tgz",
"integrity": "sha512-GVzdlWoZWldRDzlWj3SttRXmVDxg7YfraAohwy+o9gb9hrbDJaaAV6jV3pc630Evd3K46OAzk8EFu8EgPDuVuA==",
"license": "MIT",
"dependencies": {
"@react-pdf/fns": "3.1.2",
"@react-pdf/image": "^3.0.3",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/stylesheet": "^6.1.1",
"@react-pdf/textkit": "^6.0.0",
"@react-pdf/types": "^2.9.1",
"emoji-regex-xs": "^1.0.0",
"queue": "^6.0.1",
"yoga-layout": "^3.2.1"
}
},
"node_modules/@react-pdf/pdfkit": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-4.0.4.tgz",
"integrity": "sha512-/nITLggsPlB66bVLnm0X7MNdKQxXelLGZG6zB5acF5cCgkFwmXHnLNyxYOUD4GMOMg1HOPShXDKWrwk2ZeHsvw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/png-js": "^3.0.0",
"browserify-zlib": "^0.2.0",
"crypto-js": "^4.2.0",
"fontkit": "^2.0.2",
"jay-peg": "^1.1.1",
"linebreak": "^1.1.0",
"vite-compatible-readable-stream": "^3.6.1"
}
},
"node_modules/@react-pdf/png-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz",
"integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==",
"license": "MIT",
"dependencies": {
"browserify-zlib": "^0.2.0"
}
},
"node_modules/@react-pdf/primitives": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.1.1.tgz",
"integrity": "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==",
"license": "MIT"
},
"node_modules/@react-pdf/reconciler": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-1.1.4.tgz",
"integrity": "sha512-oTQDiR/t4Z/Guxac88IavpU2UgN7eR0RMI9DRKvKnvPz2DUasGjXfChAdMqDNmJJxxV26mMy9xQOUV2UU5/okg==",
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
"scheduler": "0.25.0-rc-603e6108-20241029"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-pdf/reconciler/node_modules/scheduler": {
"version": "0.25.0-rc-603e6108-20241029",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz",
"integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==",
"license": "MIT"
},
"node_modules/@react-pdf/render": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.3.1.tgz",
"integrity": "sha512-v1WAaAhQShQZGcBxfjkEThGCHVH9CSuitrZ1bIOLvB5iBKM14abYK5D6djKhWCwF6FTzYeT2WRjRMVgze/ND2A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/fns": "3.1.2",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/textkit": "^6.0.0",
"@react-pdf/types": "^2.9.1",
"abs-svg-path": "^0.1.1",
"color-string": "^1.9.1",
"normalize-svg-path": "^1.1.0",
"parse-svg-path": "^0.1.2",
"svg-arc-to-cubic-bezier": "^3.2.0"
}
},
"node_modules/@react-pdf/renderer": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.3.1.tgz",
"integrity": "sha512-dPKHiwGTaOsKqNWCHPYYrx8CDfAGsUnV4tvRsEu0VPGxuot1AOq/M+YgfN/Pb+MeXCTe2/lv6NvA8haUtj3tsA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/fns": "3.1.2",
"@react-pdf/font": "^4.0.3",
"@react-pdf/layout": "^4.4.1",
"@react-pdf/pdfkit": "^4.0.4",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/reconciler": "^1.1.4",
"@react-pdf/render": "^4.3.1",
"@react-pdf/types": "^2.9.1",
"events": "^3.3.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"queue": "^6.0.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-pdf/stylesheet": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.1.tgz",
"integrity": "sha512-Iyw0A3wRIeQLN4EkaKf8yF9MvdMxiZ8JjoyzLzDHSxnKYoOA4UGu84veCb8dT9N8MxY5x7a0BUv/avTe586Plg==",
"license": "MIT",
"dependencies": {
"@react-pdf/fns": "3.1.2",
"@react-pdf/types": "^2.9.1",
"color-string": "^1.9.1",
"hsl-to-hex": "^1.0.0",
"media-engine": "^1.0.3",
"postcss-value-parser": "^4.1.0"
}
},
"node_modules/@react-pdf/textkit": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.0.0.tgz",
"integrity": "sha512-fDt19KWaJRK/n2AaFoVm31hgGmpygmTV7LsHGJNGZkgzXcFyLsx+XUl63DTDPH3iqxj3xUX128t104GtOz8tTw==",
"license": "MIT",
"dependencies": {
"@react-pdf/fns": "3.1.2",
"bidi-js": "^1.0.2",
"hyphen": "^1.6.4",
"unicode-properties": "^1.4.1"
}
},
"node_modules/@react-pdf/types": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.9.1.tgz",
"integrity": "sha512-5GoCgG0G5NMgpPuHbKG2xcVRQt7+E5pg3IyzVIIozKG3nLcnsXW4zy25vG1ZBQA0jmo39q34au/sOnL/0d1A4w==",
"license": "MIT",
"dependencies": {
"@react-pdf/font": "^4.0.3",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/stylesheet": "^6.1.1"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1647,13 +1820,6 @@
"@types/react": "*"
}
},
"node_modules/@types/inputmask": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz",
"integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2269,6 +2435,12 @@
"win32"
]
},
"node_modules/abs-svg-path": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
"integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==",
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2603,6 +2775,35 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"license": "MIT",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -2627,6 +2828,24 @@
"node": ">=8"
}
},
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/browserify-zlib": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"license": "MIT",
"dependencies": {
"pako": "~1.0.5"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -2728,6 +2947,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2754,9 +2982,18 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2813,6 +3050,12 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -3003,6 +3246,12 @@
"node": ">=8"
}
},
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3047,6 +3296,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/emoji-regex-xs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
"integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@@ -3680,11 +3935,19 @@
"node": ">=0.10.0"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -3843,6 +4106,23 @@
}
}
},
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -4196,6 +4476,21 @@
"react-is": "^16.7.0"
}
},
"node_modules/hsl-to-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz",
"integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==",
"license": "MIT",
"dependencies": {
"hsl-to-rgb-for-reals": "^1.1.0"
}
},
"node_modules/hsl-to-rgb-for-reals": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
"license": "ISC"
},
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
@@ -4212,6 +4507,12 @@
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/hyphen": {
"version": "1.10.6",
"resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz",
"integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==",
"license": "ISC"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4248,11 +4549,11 @@
"node": ">=0.8.19"
}
},
"node_modules/inputmask": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz",
"integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==",
"license": "MIT"
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/internal-slot": {
"version": "1.1.0",
@@ -4630,6 +4931,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/is-weakmap": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -4708,6 +5015,15 @@
"node": ">= 0.4"
}
},
"node_modules/jay-peg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz",
"integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==",
"license": "MIT",
"dependencies": {
"restructure": "^3.0.0"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -4725,9 +5041,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5110,6 +5426,25 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -5182,6 +5517,12 @@
"node": ">= 0.4"
}
},
"node_modules/media-engine": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
"integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==",
"license": "MIT"
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
@@ -5392,6 +5733,15 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/normalize-svg-path": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz",
"integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==",
"license": "MIT",
"dependencies": {
"svg-arc-to-cubic-bezier": "^3.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5582,6 +5932,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5612,6 +5968,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse-svg-path": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
"integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==",
"license": "MIT"
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5705,6 +6067,12 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5764,6 +6132,15 @@
"node": ">=6"
}
},
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"license": "MIT",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5970,6 +6347,15 @@
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
"license": "MIT"
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -6009,6 +6395,12 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
"license": "MIT"
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -6064,6 +6456,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -6309,6 +6721,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -6348,6 +6775,15 @@
"node": ">= 0.4"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -6538,6 +6974,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg-arc-to-cubic-bezier": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==",
"license": "ISC"
},
"node_modules/swr": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
@@ -6588,6 +7030,12 @@
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"license": "MIT"
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@@ -6836,6 +7284,32 @@
"dev": true,
"license": "MIT"
},
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicode-trie/node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/unrs-resolver": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
@@ -6916,6 +7390,26 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"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",
"integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7053,6 +7547,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yoga-layout": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
"license": "MIT"
},
"node_modules/yup": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz",
+1 -2
View File
@@ -11,12 +11,12 @@
"format": "prettier --write ."
},
"dependencies": {
"@react-pdf/renderer": "^4.3.1",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.12.2",
"clsx": "^2.1.1",
"formik": "^2.4.6",
"inputmask": "^5.0.9",
"moment": "^2.30.1",
"next": "15.5.3",
"react": "19.1.0",
@@ -36,7 +36,6 @@
"@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4",
"@types/inputmask": "^5.0.7",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -0,0 +1,54 @@
'use client';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const EditMarketingDelivery = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('marketingId');
const {
data: marketing,
isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(`get-so-${soId}`, () =>
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
);
if (!soId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoading && (!marketing || isResponseError(marketing))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<MarketingForm
formType='add_deliver'
initialValues={marketing.data}
afterSubmit={() => {
refreshMarketing();
}}
/>
)}
</div>
);
};
export default EditMarketingDelivery;
@@ -1,9 +1,9 @@
import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
const AddSalesOrder = () => {
return (
<div className='size-full p-4'>
<SalesForm />
<MarketingForm formType='add' />
</div>
);
};
@@ -0,0 +1,62 @@
'use client';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const EditMarketingDelivery = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('marketingId');
const {
data: marketing,
isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(`get-so-${soId}`, () =>
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
);
if (!soId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoading && (!marketing || isResponseError(marketing))) {
router.replace('/404');
return;
}
if (
isResponseSuccess(marketing) &&
marketing.data.latest_approval.step_number != 3
) {
toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!');
router.back();
}
return (
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<MarketingForm
formType='edit_deliver'
initialValues={marketing.data}
afterSubmit={() => {
refreshMarketing();
}}
/>
)}
</div>
);
};
export default EditMarketingDelivery;
+11
View File
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -1,20 +1,22 @@
'use client';
import SalesOrderDetail from '@/components/pages/marketing/sales-orders/detail/SalesOrderDetail';
import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const DetailSalesOrder = () => {
const DetailMarketing = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('salesOrderId');
const soId = searchParams.get('marketingId');
const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
MarketingApi.getSingle(id)
);
const {
data: marketing,
isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
if (!soId) {
router.back();
@@ -35,10 +37,13 @@ const DetailSalesOrder = () => {
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<SalesOrderDetail initialValues={marketing.data} />
<MarketingDetail
initialValues={marketing.data}
refresh={refreshMarketing}
/>
)}
</div>
);
};
export default DetailSalesOrder;
export default DetailMarketing;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -1,6 +1,6 @@
'use client';
import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
@@ -10,10 +10,14 @@ const EditSalesOrder = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('salesOrderId');
const soId = searchParams.get('marketingId');
const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
MarketingApi.getSingle(id)
const {
data: marketing,
isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(`get-so-${soId}`, () =>
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
);
if (!soId) {
@@ -34,7 +38,13 @@ const EditSalesOrder = () => {
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<SalesForm formType='edit' initialValues={marketing.data} />
<MarketingForm
formType='edit'
initialValues={marketing.data}
afterSubmit={() => {
refreshMarketing();
}}
/>
)}
</div>
);
+10
View File
@@ -0,0 +1,10 @@
import MarketingTable from '@/components/pages/marketing/MarketingTable';
const Marketing = () => {
return (
<div className='w-full p-4'>
<MarketingTable />
</div>
);
};
export default Marketing;
-10
View File
@@ -1,10 +0,0 @@
import SalesOrderTable from '@/components/pages/marketing/sales-orders/SalesOrderTable';
const SalesOrder = () => {
return (
<div className='w-full p-4'>
<SalesOrderTable />
</div>
);
};
export default SalesOrder;
@@ -11,10 +11,6 @@ const AddChickin = () => {
return (
<>
<section className='w-full p-4'>
<FormHeader
title='Daftar Kandang Project Flock'
backUrl='/production/project-flock'
/>
<ProjectFlockChickinDetail projectFlockId={Number(projectFlockId)} />
</section>
</>
@@ -1,343 +0,0 @@
'use client';
import Button from '@/components/Button';
import Card from '@/components/Card';
import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { ChickinApi } from '@/services/api/production/chickin';
import { BaseApiResponse } from '@/types/api/api-general';
import {
Chickin,
ChickinApprovalPayload,
} from '@/types/api/production/chickin';
import { Icon } from '@iconify/react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
/**
* TODO: Refactor code - pindahin detail ke reuseable component
* setelah implement approval and reject
*/
const DetailChickin = () => {
const router = useRouter();
const searchParams = useSearchParams();
const chickinId = searchParams.get('chickinId');
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const confirmModal = useModal();
const deleteModal = useModal();
const chickinModal = useModal();
const {
data: chickin,
isLoading,
mutate: refreshChickin,
} = useSWR(chickinId, (id: number) => ChickinApi.getSingle(id));
const [isApprovedDisabled, setIsApprovedDisabled] = useState(
// chickin.data?.approval.step_number == 1 ? false : true
true
);
const [isRejectedDisabled, setIsRejectedDisabled] =
useState(!isApprovedDisabled);
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
!isApprovedDisabled ? 'APPROVED' : 'REJECTED'
);
if (!chickinId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoading && (!chickin || isResponseError(chickin))) {
router.replace('/404');
return;
}
if (!isResponseSuccess(chickin)) {
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
const confirmationModalClickHandler = async ({
action = 'APPROVED',
}: {
action: 'APPROVED' | 'REJECTED';
}) => {
if (chickin?.data.id === undefined) return;
setIsApproveLoading(true);
const approveChickinRes = await ChickinApi.customRequest<
BaseApiResponse<Chickin>,
ChickinApprovalPayload
>(`/approvals`, {
method: 'POST',
payload: {
action: action,
approvable_ids: [chickin.data.id],
},
});
if (isResponseSuccess(approveChickinRes)) {
if (refreshChickin) {
await refreshChickin();
}
toast.success(approveChickinRes.message as string);
}
if (isResponseError(approveChickinRes)) {
toast.error(approveChickinRes?.message as string);
}
confirmModal.closeModal();
setIsApproveLoading(false);
};
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
const deleteProjectFlockRes = await ChickinApi.delete(
chickin.data?.id as number
);
if (isResponseSuccess(deleteProjectFlockRes)) {
toast.success(deleteProjectFlockRes?.message as string);
router.push('/production/chickin');
}
if (isResponseError(deleteProjectFlockRes)) {
toast.error(deleteProjectFlockRes?.message as string);
}
deleteModal.closeModal();
setIsDeleteLoading(false);
};
return (
<>
<div className='w-full p-4 flex flex-col justify-center gap-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(chickin) && (
<>
{/* <div className='w-full flex flex-col sm:flex-row gap-2'>
<Button
variant='outline'
color='success'
onClick={(() => {
if (chickin?.data.id) {
setApprovalAction('APPROVED');
confirmModal.openModal();
}
})}
disabled={!chickin?.data.id || isApprovedDisabled}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<Button
variant='outline'
color='error'
onClick={() => {
if (chickin?.data.id) {
setApprovalAction('REJECTED');
confirmModal.openModal();
}
}}
disabled={!chickin?.data.id || isRejectedDisabled}
className='w-full sm:w-fit'
>
<Icon icon='mdi:times' width={24} height={24} />
Reject
</Button>
</div> */}
<Card
title='Informasi Umum'
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<div className='grid grid-cols-2 gap-4 mt-4'>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Flock</div>
<div className='text-sm'>
{
chickin?.data?.project_flock_kandang?.project_flock?.flock
?.name
}
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Area</div>
<div className='text-sm'>
{
chickin.data.project_flock_kandang?.project_flock.area
.name
}
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Kategori</div>
<div className='text-sm'>
{chickin.data.project_flock_kandang?.project_flock.category}
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Lokasi</div>
<div className='text-sm'>
{
chickin.data.project_flock_kandang?.project_flock.location
.name
}
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Periode</div>
<div className='text-sm'>
{chickin.data.project_flock_kandang?.project_flock.period}
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Kandang</div>
<div className='text-sm'>
{chickin.data.project_flock_kandang?.kandang.name}
</div>
</div>
</div>
</Card>
<Card
title='Detail Chickin'
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<div className='grid grid-cols-2 gap-4 mt-4'>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Flock Kandang</div>
<div className='text-sm'>
{
chickin?.data?.project_flock_kandang?.project_flock?.flock
?.name
}{' '}
- {chickin.data.project_flock_kandang?.kandang.name}
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Tanggal Chickin</div>
<div className='text-sm'>
{chickin.data.chick_in_date
? new Date(chickin.data.chick_in_date).toLocaleDateString(
'id-ID'
)
: '-'}
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Jumlah (Ekor)</div>
<div className='text-sm'>
{chickin.data.quantity?.toLocaleString('id-ID')}
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='font-semibold text-sm'>Catatan</div>
<div className='text-sm'>{chickin.data.note}</div>
</div>
</div>
</Card>
<div className='w-full flex flex-col sm:flex-row gap-2'>
<Button
color='error'
onClick={() => {
deleteModal.openModal();
}}
>
<Icon icon='mdi:times' width={24} height={24} />
Delete
</Button>
<Button
color='warning'
onClick={() => {
chickinModal.openModal();
}}
>
<Icon icon='mdi:pencil-outline' width={24} height={24} />
Edit
</Button>
</div>
</>
)}
</div>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${chickin?.data?.project_flock_kandang?.project_flock.flock?.name} - ${chickin?.data.project_flock_kandang?.kandang.name})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
<Modal ref={chickinModal.ref}>
<div className='flex flex-row justify-between items-center'>
<h1 className='text-xl font-semibold text-center mb-6'>
Chickin Kandang -{' '}
{chickin?.data?.project_flock_kandang &&
chickin?.data?.project_flock_kandang.kandang?.name}
</h1>
<Button
color='error'
variant='link'
onClick={chickinModal.closeModal}
>
<Icon
className='text-black'
icon='uil:times'
width={24}
height={24}
/>
</Button>
</div>
</Modal>
<ConfirmationModal
ref={confirmModal.ref}
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${
approvalAction == 'APPROVED' ? 'approve' : 'reject'
} chickin berikut? (${
chickin?.data?.project_flock_kandang?.project_flock?.flock?.name
} - ${chickin?.data.project_flock_kandang?.kandang.name})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: approvalAction == 'APPROVED' ? 'success' : 'error',
isLoading: isApproveLoading,
onClick: () => {
confirmationModalClickHandler({
action: approvalAction,
});
},
}}
/>
</>
);
};
export default DetailChickin;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
const AddGrading = () => {
const router = useRouter();
const searchParams = useSearchParams();
const recordingId = searchParams.get('recording_id');
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId && recordingId !== 'new' ? [recordingId] : null,
([id]) => RecordingApi.getSingle(parseInt(id))
);
if (
recordingId &&
recordingId !== 'new' &&
!isLoadingRecording &&
(!recording || !isResponseSuccess(recording))
) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{recordingId && recordingId !== 'new' && isLoadingRecording && (
<span className='loading loading-spinner loading-xl' />
)}
{(!recordingId ||
recordingId === 'new' ||
(!isLoadingRecording && recording && isResponseSuccess(recording))) && (
<GradingForm
type='add'
initialValues={
isResponseSuccess(recording) ? recording.data?.eggs?.[0] : undefined
}
/>
)}
</div>
);
};
export default AddGrading;
@@ -0,0 +1,53 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
const EditGrading = () => {
const router = useRouter();
const searchParams = useSearchParams();
const recordingId = searchParams.get('recordingId');
const gradingId = searchParams.get('gradingId');
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId ? [recordingId] : null,
([id]) => RecordingApi.getSingle(parseInt(id))
);
if (!recordingId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingRecording && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingRecording && recording && isResponseSuccess(recording) && (
<GradingForm
type='edit'
initialValues={recording.data.eggs?.find(
(egg) => egg.id === parseInt(gradingId || '0')
)}
/>
)}
</div>
);
};
export default EditGrading;
@@ -0,0 +1,52 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
const DetailGrading = () => {
const router = useRouter();
const searchParams = useSearchParams();
const gradingId = searchParams.get('gradingId');
const { data: grading, isLoading: isLoadingGrading } = useSWR(
gradingId ? [gradingId] : null,
([id]) => RecordingApi.getSingle(parseInt(id))
);
if (!gradingId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingGrading && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingGrading && grading && isResponseSuccess(grading) && (
<GradingForm
type='detail'
initialValues={grading.data.eggs?.find(
(egg) => egg.id === parseInt(gradingId)
)}
/>
)}
</div>
);
};
export default DetailGrading;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
+3 -2
View File
@@ -3,6 +3,7 @@
import { HTMLAttributes, ReactNode } from 'react';
import { cn } from '@/lib/helper';
import Image from 'next/image';
export interface CardProps
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
@@ -106,7 +107,7 @@ const Card = ({
return (
<div className={getCardClasses()} {...props}>
<figure>
<img
<Image
src={image}
alt={imageAlt || title || 'Card image'}
className={getImageClasses()}
@@ -127,7 +128,7 @@ const Card = ({
<div className={getCardClasses()} {...props}>
{image && (
<figure>
<img
<Image
src={image}
alt={imageAlt || title || 'Card image'}
className={getImageClasses()}
+67 -15
View File
@@ -9,6 +9,11 @@ interface FormActionsProps<T> {
editUrl?: string;
onDelete?: () => void;
disableSubmit?: boolean;
onApprove?: () => void;
onReject?: () => void;
isApproveLoading?: boolean;
isRejectLoading?: boolean;
showApproveReject?: boolean;
}
export const FormActions = <T,>({
@@ -17,25 +22,32 @@ export const FormActions = <T,>({
editUrl,
onDelete,
disableSubmit = false,
onApprove,
onReject,
isApproveLoading = false,
isRejectLoading = false,
showApproveReject = false,
}: FormActionsProps<T>) => {
return (
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && onDelete && (
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={onDelete}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{onDelete && (
<Button
type='button'
color='error'
onClick={onDelete}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
)}
{type !== 'edit' && editUrl && (
<Button
type='button'
@@ -52,6 +64,46 @@ export const FormActions = <T,>({
Edit
</Button>
)}
{type === 'detail' &&
showApproveReject &&
(onApprove || onReject) && (
<>
{onApprove && (
<Button
type='button'
color='success'
onClick={onApprove}
className='px-4'
isLoading={isApproveLoading}
>
<Icon
icon='material-symbols:check-circle-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Approve
</Button>
)}
{onReject && (
<Button
type='button'
color='error'
onClick={onReject}
className='px-4'
isLoading={isRejectLoading}
>
<Icon
icon='material-symbols:cancel-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Reject
</Button>
)}
</>
)}
</div>
)}
{type !== 'detail' && (
+4 -1
View File
@@ -78,7 +78,10 @@ const DateInput = ({
// --- Sync value props ---
useEffect(() => {
if (!value) return;
if (!value) {
setDisplayValue('');
return;
}
if (isRange && typeof value === 'object') {
const from = value.from ? new Date(value.from) : undefined;
const to = value.to ? new Date(value.to) : undefined;
+2 -2
View File
@@ -49,8 +49,8 @@ const NumberInput = ({
onValueChange={valueChangeHandler}
decimalScale={decimalScale}
allowNegative={allowNegative}
startAdornment={inputPrefix}
endAdornment={inputSuffix}
inputPrefix={inputPrefix}
inputSuffix={inputSuffix}
{...restProps}
/>
);
+1 -1
View File
@@ -83,7 +83,7 @@ const TextArea = ({
<textarea
className={cn(
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all bg-white',
'textarea h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all bg-white',
{
'border-error': isError,
'border-success!': isValid,
+111 -29
View File
@@ -31,6 +31,8 @@ export interface TextInputProps {
errorMessage?: string;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
inputPrefix?: ReactNode;
inputSuffix?: ReactNode;
onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
}
@@ -48,6 +50,8 @@ const TextInput = ({
errorMessage,
startAdornment,
endAdornment,
inputPrefix,
inputSuffix,
disabled = false,
required = false,
onChange,
@@ -85,39 +89,117 @@ const TextInput = ({
</label>
)}
<div
className={cn(
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all duration-200',
{
'border-error': isError,
'border-success!': isValid,
},
className?.inputWrapper
)}
>
{startAdornment && startAdornment}
{inputPrefix || inputSuffix ? (
<div className='relative flex'>
{inputPrefix && (
<div
className={cn(
'inline-flex items-center px-4 py-2 border border-r-0 rounded-l-md transition-all duration-200',
{
'bg-gray-100 border-gray-300': !disabled,
'bg-gray-50 border-gray-200': disabled,
}
)}
>
{inputPrefix}
</div>
)}
<input
type={type}
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('grow', className?.input)}
readOnly={readOnly}
/>
<div
className={cn(
'input h-12 text-base font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white',
{
'border-error': isError,
'border-success!': isValid,
'rounded-l-none!': inputPrefix,
'rounded-r-none!': inputSuffix,
'input-disabled': disabled,
'cursor-not-allowed': disabled,
'bg-gray-50': disabled,
},
className?.inputWrapper
)}
>
{startAdornment && startAdornment}
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
<input
type={type}
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn(
'grow bg-transparent outline-none',
{
'cursor-not-allowed': disabled,
'text-gray-500': disabled,
},
className?.input
)}
readOnly={readOnly}
/>
{endAdornment && endAdornment}
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
</div>
)}
</div>
{inputSuffix && (
<div
className={cn(
'inline-flex items-center px-4 py-2 border border-l-0 rounded-r-md transition-all duration-200',
{
'bg-gray-100 border-gray-300': !disabled,
'bg-gray-50 border-gray-200': disabled,
}
)}
>
{inputSuffix}
</div>
)}
</div>
) : (
<div
className={cn(
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white',
{
'border-error': isError,
'border-success!': isValid,
},
className?.inputWrapper
)}
>
{startAdornment && startAdornment}
<input
type={type}
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('grow', className?.input)}
readOnly={readOnly}
/>
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
</div>
)}
{!isError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
@@ -50,6 +50,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
...primaryButton,
onClick: () => {
primaryButton?.onClick?.(notes);
setNotes('');
},
}}
secondaryButton={secondaryButton}
+1 -1
View File
@@ -298,7 +298,7 @@ const useApprovalSteps = ({
moduleName: string;
moduleId: string;
params?: {
page: number;
page?: number;
limit: number;
search?: string;
group_step_number?: boolean;
@@ -1,24 +1,46 @@
'use client';
import { useState } from 'react';
import { ChangeEventHandler, useState } from 'react';
import useSWR from 'swr';
import { SortingState } from '@tanstack/react-table';
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
import Table from '@/components/Table';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { Icon } from '@iconify/react';
import { Movement } from '@/types/api/inventory/movement';
import { MovementApi } from '@/services/api/inventory';
import { cn } from '@/lib/helper';
import { Product } from '@/types/api/master-data/product';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
import { TableToolbar } from '@/components/table/TableToolbar';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import { OptionType } from '@/components/input/SelectInput';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import { TableRowOptions } from '@/components/table/TableRowOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
const RowOptionsMenu = ({
type = 'dropdown',
props,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Movement, unknown>;
}) => (
<RowOptionsMenuWrapper type={type}>
<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>
);
const MovementTable = () => {
const {
@@ -28,30 +50,47 @@ const MovementTable = () => {
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: { search: '' },
paramMap: { page: 'page', pageSize: 'limit' },
initial: {
search: '',
product: '',
warehouse: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
product: 'product_id',
warehouse: 'warehouse_id',
},
});
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedMovement, setSelectedMovement] = useState<
Movement | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const deleteModal = useModal();
const {
data: movements,
isLoading,
mutate: refreshMovements,
} = useSWR(
setInputValue: setProductInputValue,
options: productOptions,
isLoadingOptions: isLoadingProductOptions,
} = useSelect<Product>('/products', 'id', 'name');
const {
setInputValue: setWarehouseInputValue,
options: warehouseOptions,
isLoadingOptions: isLoadingWarehouseOptions,
} = useSelect<Warehouse>('/warehouses', 'id', 'name');
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
null
);
const [selectedWarehouse, setSelectedWarehouse] = useState<OptionType | null>(
null
);
const { data: movements, isLoading } = useSWR(
`${MovementApi.basePath}${getTableFilterQueryString()}`,
MovementApi.getAllFetcher
);
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
setPage(1);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -60,167 +99,179 @@ const MovementTable = () => {
setPage(1);
};
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
try {
await MovementApi.delete(selectedMovement?.id as number);
refreshMovements();
deleteModal.closeModal();
} finally {
setIsDeleteLoading(false);
}
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedProduct(val as OptionType);
updateFilter('product', val ? ((val as OptionType).value as string) : '');
};
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedWarehouse(val as OptionType);
updateFilter('warehouse', val ? ((val as OptionType).value as string) : '');
};
const movementColumns: ColumnDef<Movement>[] = [
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorFn: (row) => row.source_warehouse?.name,
header: 'Gudang Asal',
},
{
accessorFn: (row) => row.destination_warehouse?.name,
header: 'Gudang Tujuan',
},
{
accessorKey: 'transfer_reason',
header: 'Catatan',
},
{
accessorKey: 'transfer_date',
header: 'Tanggal',
cell: (props) =>
new Date(props.row.original.transfer_date).toLocaleDateString('id-ID'),
},
{
accessorFn: (row) => {
const totalCost = row.deliveries?.reduce(
(sum, d) => sum + (d.shipping_cost_total || 0),
0
);
return totalCost?.toLocaleString('id-ID');
},
header: 'Biaya Pengiriman',
},
{
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 - 2;
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu type='dropdown' props={props} />
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu type='collapse' props={props} />
</RowCollapseOptions>
)}
</>
);
},
},
];
return (
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2 mb-4'>
<TableToolbar
addButton={{
href: '/inventory/movement/add',
label: 'Tambah',
<>
<div className='w-full p-0 sm:p-4'>
<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'>
<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
name='search'
placeholder='Cari Movement'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
<div className='grid grid-cols-12 justify-end gap-4'>
<SelectInput
label='Produk'
options={productOptions}
isLoading={isLoadingProductOptions}
value={selectedProduct}
onChange={productChangeHandler}
onInputChange={setProductInputValue}
isClearable
className={{
wrapper: 'col-span-12 sm:col-span-4',
}}
/>
<SelectInput
label='Gudang'
options={warehouseOptions}
isLoading={isLoadingWarehouseOptions}
value={selectedWarehouse}
onChange={warehouseChangeHandler}
onInputChange={setWarehouseInputValue}
isClearable
className={{
wrapper: 'col-span-12 sm:col-span-4',
}}
/>
<SelectInput
label='Baris'
options={ROWS_OPTIONS}
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}}
onChange={pageSizeChangeHandler}
className={{
wrapper:
'col-span-6 sm:col-span-4 max-w-28 sm:justify-self-end',
}}
/>
</div>
</div>
<Table<Movement>
data={isResponseSuccess(movements) ? movements?.data : []}
columns={movementColumns}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
totalItems={
isResponseSuccess(movements) ? movements?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(movements) && movements?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
search={{
value: tableFilterState.search,
onChange: searchChangeHandler,
placeholder: 'Cari Movement',
}}
/>
<TableRowSizeSelector
value={tableFilterState.pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
/>
</div>
<Table<Movement>
data={isResponseSuccess(movements) ? movements?.data : []}
columns={[
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorFn: (row) => row.source_warehouse?.name,
header: 'Gudang Asal',
},
{
accessorFn: (row) => row.destination_warehouse?.name,
header: 'Gudang Tujuan',
},
{
accessorKey: 'transfer_reason',
header: 'Catatan',
},
{
accessorKey: 'transfer_date',
header: 'Tanggal',
cell: (props) =>
new Date(props.row.original.transfer_date).toLocaleDateString(
'id-ID'
),
},
{
accessorFn: (row) => {
const totalCost = row.deliveries?.reduce(
(sum, d) => sum + (d.shipping_cost_total || 0),
0
);
return totalCost?.toLocaleString('id-ID');
},
header: 'Biaya Pengiriman',
},
{
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 - 2;
const deleteClickHandler = () => {
setSelectedMovement(props.row.original);
deleteModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<TableRowOptions
type='dropdown'
recordId={props.row.original.id}
basePath='/inventory/movement'
queryParam='movementId'
showEdit={false}
showDelete={false}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<TableRowOptions
type='collapse'
recordId={props.row.original.id}
basePath='/inventory/movement'
queryParam='movementId'
showEdit={false}
showDelete={false}
/>
</RowCollapseOptions>
)}
</>
);
},
},
]}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
totalItems={
isResponseSuccess(movements) ? movements?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(movements) && movements?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
</div>
</>
);
};
@@ -1,34 +1,82 @@
import * as Yup from 'yup';
import { Movement } from '@/types/api/inventory/movement';
type MovementFormSchemaType = {
transfer_reason: string;
transfer_date: string;
source_warehouse?: {
value: number;
label: string;
area?: string;
location?: string;
} | null;
source_warehouse_id: number;
destination_warehouse?: {
value: number;
label: string;
area?: string;
location?: string;
} | null;
destination_warehouse_id: number;
products: {
product?: {
value: number;
label: string;
} | null;
product_id: number;
product_qty: number | string;
}[];
deliveries: {
delivery_cost?: number | string;
delivery_cost_per_item?: number | string;
document?: File | string | null;
document_path?: string | null;
driver_name: string;
vehicle_plate: string;
supplier?: {
value: number;
label: string;
} | null;
supplier_id: number;
products: {
product?: {
value: number;
label: string;
} | null;
product_id: number;
product_qty: number | string;
}[];
}[];
};
export type ProductSchema = {
product: {
product?: {
value: number;
label: string;
} | null;
product_id: number;
product_qty: number;
product_qty: number | string;
};
export type DeliverySchema = {
delivery_cost?: number | undefined;
delivery_cost_per_item?: number | undefined;
delivery_cost?: number | string;
delivery_cost_per_item?: number | string;
document?: File | string | null;
document_path?: string | null;
driver_name: string;
vehicle_plate: string;
supplier: {
supplier?: {
value: number;
label: string;
} | null;
supplier_id: number;
products: {
product: {
product?: {
value: number;
label: string;
} | null;
product_id: number;
product_qty: number;
product_qty: number | string;
}[];
};
@@ -102,38 +150,37 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
.required('Produk wajib diisi!'),
});
export const MovementFormSchema = Yup.object({
transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
source_warehouse: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
area: Yup.string().optional(),
location: Yup.string().optional(),
}).nullable(),
source_warehouse_id: Yup.number()
.required('Gudang asal wajib diisi!')
.typeError('Gudang asal wajib diisi!'),
destination_warehouse: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
area: Yup.string().optional(),
location: Yup.string().optional(),
}).nullable(),
destination_warehouse_id: Yup.number()
.required('Gudang tujuan wajib diisi!')
.typeError('Gudang tujuan wajib diisi!'),
products: Yup.array()
.of(ProductObjectSchema)
.min(1, 'Minimal harus ada 1 produk!')
.required('Produk wajib diisi!'),
deliveries: Yup.array()
.of(DeliveryObjectSchema)
.min(1, 'Minimal harus ada 1 pengiriman!')
.required('Pengiriman wajib diisi!'),
});
export const UpdateMovementFormSchema = MovementFormSchema;
export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> =
Yup.object({
transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
source_warehouse: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
area: Yup.string().optional(),
location: Yup.string().optional(),
}).nullable(),
source_warehouse_id: Yup.number()
.required('Gudang asal wajib diisi!')
.typeError('Gudang asal wajib diisi!'),
destination_warehouse: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
area: Yup.string().optional(),
location: Yup.string().optional(),
}).nullable(),
destination_warehouse_id: Yup.number()
.required('Gudang tujuan wajib diisi!')
.typeError('Gudang tujuan wajib diisi!'),
products: Yup.array()
.of(ProductObjectSchema)
.min(1, 'Minimal harus ada 1 produk!')
.required('Produk wajib diisi!'),
deliveries: Yup.array()
.of(DeliveryObjectSchema)
.min(1, 'Minimal harus ada 1 pengiriman!')
.required('Pengiriman wajib diisi!'),
});
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
File diff suppressed because it is too large Load Diff
@@ -1,95 +0,0 @@
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
import { useModal } from '@/components/Modal';
import { MovementApi } from '@/services/api/inventory';
import {
CreateMovementPayload,
UpdateMovementPayload,
} from '@/types/api/inventory/movement';
import { isResponseError } from '@/lib/api-helper';
export const useMovementFormHandlers = (initialValuesId?: number) => {
const router = useRouter();
const deleteModal = useModal();
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const createMovementHandler = useCallback(
async (payload: CreateMovementPayload, documents: File[] = []) => {
const formData = new FormData();
formData.append('data', JSON.stringify(payload));
documents.forEach((file, index) => {
formData.append(`documents[${index}]`, file);
});
const res = await MovementApi.create(
formData as unknown as CreateMovementPayload
);
if (isResponseError(res)) {
setMovementFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.push('/inventory/movement');
},
[router]
);
const updateMovementHandler = useCallback(
async (
movementId: number,
payload: UpdateMovementPayload,
documents: File[] = []
) => {
let finalPayload: UpdateMovementPayload | FormData;
if (documents.length > 0) {
const formData = new FormData();
formData.append('data', JSON.stringify(payload));
documents.forEach((file, index) => {
formData.append(`documents[${index}]`, file);
});
finalPayload = formData as unknown as UpdateMovementPayload;
} else {
finalPayload = payload;
}
const res = await MovementApi.update(movementId, finalPayload);
if (res?.status === 'error') {
setMovementFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.refresh();
router.push('/inventory/movement');
},
[router]
);
const deleteMovementClickHandler = useCallback(() => {
deleteModal.openModal();
}, [deleteModal]);
const confirmationModalDeleteClickHandler = useCallback(async () => {
if (!initialValuesId) return;
setIsDeleteLoading(true);
await MovementApi.delete(initialValuesId);
deleteModal.closeModal();
toast.success('Successfully delete Movement!');
setIsDeleteLoading(false);
router.push('/inventory/movement');
}, [deleteModal, initialValuesId, router]);
return {
deleteModal,
movementFormErrorMessage,
isDeleteLoading,
createMovementHandler,
updateMovementHandler,
deleteMovementClickHandler,
confirmationModalDeleteClickHandler,
};
};
@@ -5,31 +5,38 @@ import CheckboxInput from '@/components/input/CheckboxInput';
import { OptionType } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import { TableToolbar } from '@/components/table/TableToolbar';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency, formatVechicleNumber } from '@/lib/helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import {
MarketingApi,
SalesOrderApi,
} from '@/services/api/marketing/marketing';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing';
import { Customer } from '@/types/api/master-data/customer';
import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { CellContext } from '@tanstack/react-table';
import { CellContext, Row } from '@tanstack/react-table';
import { useRouter } from 'next/navigation';
import { useCallback, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const RowsOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
deliveryClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Marketing, unknown>;
deleteClickHandler: () => void;
deliveryClickHandler?: () => void;
}) => {
return (
<div
@@ -44,7 +51,7 @@ const RowsOptionsMenu = ({
>
<div className='flex flex-col gap-1'>
<Button
href={`/marketing/sales-orders/detail/?salesOrderId=${props.row.original.id}`}
href={`/marketing/detail?marketingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
@@ -52,15 +59,39 @@ const RowsOptionsMenu = ({
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/marketing/sales-orders/detail/edit/?salesOrderId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
{props.row.original.latest_approval.step_number != 1 && (
<Button
href={
props.row.original.latest_approval.step_number == 3
? `/marketing/detail/delivery-orders/edit?marketingId=${props.row.original.id}`
: props.row.original.latest_approval.step_number == 2
? `/marketing/add/delivery-orders?marketingId=${props.row.original.id}`
: undefined
}
onClick={() => {
if (props.row.original.latest_approval.step_number == 2) {
deliveryClickHandler?.();
}
}}
variant='ghost'
color='success'
className='justify-start text-sm'
>
<Icon icon='mdi:truck' width={16} height={16} />
Deliver
</Button>
)}
{props.row.original.latest_approval.step_number != 3 && (
<Button
href={`/marketing/detail/sales-orders/edit?marketingId=${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'
@@ -75,19 +106,18 @@ const RowsOptionsMenu = ({
);
};
const SalesOrderTable = () => {
const MarketingTable = () => {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [approveAction, setApproveAction] = useState<
'approve' | 'reject' | null
>(null);
const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>(
'APPROVED'
);
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection).filter(
(id) => rowSelection[id]
);
const router = useRouter();
const {
data: marketing,
@@ -98,6 +128,7 @@ const SalesOrderTable = () => {
const deleteModal = useModal();
const confirmationModal = useModal();
const productsModal = useModal();
const deliveryModal = useModal();
const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -116,12 +147,12 @@ const SalesOrderTable = () => {
);
const approveClickHandler = () => {
setApproveAction('approve');
setApproveAction('APPROVED');
confirmationModal.openModal();
};
const rejectClickHandler = () => {
setApproveAction('reject');
setApproveAction('REJECTED');
confirmationModal.openModal();
};
@@ -130,6 +161,93 @@ const SalesOrderTable = () => {
productsModal.openModal();
};
const deleteMarketingHandler = async () => {
const deleteMarketingRes = await MarketingApi.delete(
selectedItem?.id as number
);
if (isResponseSuccess(deleteMarketingRes)) {
confirmationModal.closeModal();
toast.success(deleteMarketingRes?.message as string);
}
if (isResponseError(deleteMarketingRes)) {
confirmationModal.closeModal();
toast.error(deleteMarketingRes?.message as string);
}
refreshMarketing();
deleteModal.closeModal();
};
const allData = isResponseSuccess(marketing) ? marketing.data : [];
const selectedRowsData = allData.filter(
(row) => rowSelection[row.id.toString()]
);
const hasApprovable = selectedRowsData.some(
(row) => row.latest_approval.step_number === 1
);
const hasRejectable = selectedRowsData.some(
(row) => row.latest_approval.step_number === 2
);
const disableApprove = !hasApprovable || hasRejectable;
// const disableReject = !hasRejectable || hasApprovable;
const idsToProcess =
approveAction === 'APPROVED'
? selectedRowsData
.filter((row) => row.latest_approval.step_number === 1)
.map((row) => row.id)
: selectedRowsData
.filter((row) => row.latest_approval.step_number === 2)
.map((row) => row.id);
const approveMarketingHandler = async (notes: string) => {
let idsToProcess: number[] = [];
if (approveAction === 'APPROVED') {
idsToProcess = selectedRowsData
.filter((row) => row.latest_approval.step_number === 1)
.map((row) => row.id);
} else if (approveAction === 'REJECTED') {
idsToProcess = selectedRowsData
.filter((row) => row.latest_approval.step_number === 2)
.map((row) => row.id);
}
if (idsToProcess.length === 0) {
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
confirmationModal.closeModal();
return;
}
const approveMarketingRes = await SalesOrderApi.bulkApprovals(
idsToProcess,
approveAction,
notes
);
if (isResponseSuccess(approveMarketingRes)) {
confirmationModal.closeModal();
toast.success(approveMarketingRes?.message as string);
setRowSelection({});
}
if (isResponseError(approveMarketingRes)) {
confirmationModal.closeModal();
toast.error(approveMarketingRes?.message as string);
}
refreshMarketing();
};
const confirmationModalDeliveryClickHandler = async (notes: string) => {
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
deliveryModal.closeModal();
toast.success(res?.message as string);
refreshMarketing?.();
router.push(
`/marketing/detail/delivery-orders/edit?marketingId=${selectedItem?.id}`
);
};
const {
state: tableFilterState,
updateFilter,
@@ -144,13 +262,18 @@ const SalesOrderTable = () => {
},
});
const getRowCanSelect = (row: Row<Marketing>): boolean => {
const step = row.original.latest_approval?.step_number;
return step === 1;
};
return (
<>
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2 mb-4'>
<TableToolbar
addButton={{
href: '/marketing/sales-orders/add',
href: '/marketing/add/sales-orders',
label: 'Tambah Sales Order',
}}
search={{
@@ -169,51 +292,75 @@ const SalesOrderTable = () => {
color='success'
onClick={approveClickHandler}
className='justify-start text-sm'
disabled={!selectedRowIds.length}
disabled={disableApprove}
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<Button
{/* <Button
color='error'
onClick={rejectClickHandler}
className='justify-start text-sm'
disabled={!selectedRowIds.length}
disabled={disableReject}
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
</Button> */}
</div>
</div>
<Table
rowSelection={rowSelection}
setRowSelection={setRowSelection}
data={isResponseSuccess(marketing) ? marketing.data : []}
data={allData}
columns={[
{
id: 'select',
header: ({ table }) => (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
</div>
),
cell: ({ row }) => (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
),
header: ({ table }) => {
const allRows = table.getRowModel().rows;
const selectableRows = allRows.filter(getRowCanSelect);
const allSelected =
selectableRows.length > 0 &&
selectableRows.every((row) => row.getIsSelected());
const someSelected =
selectableRows.some((row) => row.getIsSelected()) &&
!allSelected;
const toggleSelectableRows = () => {
const shouldSelect = !allSelected;
selectableRows.forEach((row) =>
row.toggleSelected(shouldSelect)
);
};
return (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={allSelected}
indeterminate={someSelected}
onChange={toggleSelectableRows}
disabled={selectableRows.length === 0}
/>
</div>
);
},
cell: ({ row }) => {
const canSelect = getRowCanSelect(row);
return (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!canSelect}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
);
},
},
{
accessorKey: 'so_number',
@@ -222,9 +369,12 @@ const SalesOrderTable = () => {
{
accessorKey: 'so_date',
header: 'Tanggal',
cell: (props) => {
return formatDate(props.row.original.so_date, 'DD MMM yyyy');
},
},
{
accessorKey: 'approval.step_name',
accessorKey: 'latest_approval.step_name',
header: 'Status',
},
{
@@ -232,15 +382,25 @@ const SalesOrderTable = () => {
header: 'Customer',
},
{
accessorKey: 'grand_total',
accessorFn: (row) =>
row.sales_order
?.map((product) => product.total_price)
.reduce((a, b) => a + b, 0) ?? 0,
header: 'Grand Total',
cell: (props) => {
return formatCurrency(
props.row.original?.sales_order
?.map((product) => product.total_price)
.reduce((a, b) => a + b, 0) ?? 0
);
},
},
{
accessorKey: 'marketing_products.length',
header: 'Product Details',
cell: (props) => {
if (props?.row?.original?.marketing_products?.length) {
if (props?.row?.original?.marketing_products?.length > 1) {
if (props?.row?.original?.sales_order?.length) {
if (props?.row?.original?.sales_order?.length > 1) {
return (
<Button
variant='link'
@@ -250,12 +410,11 @@ const SalesOrderTable = () => {
productsClickHandler(props?.row?.original);
}}
>
Lihat {props?.row?.original?.marketing_products?.length}{' '}
Produk
Lihat {props?.row?.original?.sales_order?.length} Produk
</Button>
);
} else {
const product = props?.row?.original?.marketing_products[0];
const product = props?.row?.original?.sales_order[0];
return <>{product?.product_warehouse?.product?.name}</>;
}
}
@@ -274,7 +433,15 @@ const SalesOrderTable = () => {
const isLast2Rows =
currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {};
const deleteClickHandler = () => {
setSelectedItem(props.row.original);
deleteModal.openModal();
};
const deliveryClickHandler = () => {
setSelectedItem(props.row.original);
deliveryModal.openModal();
};
return (
<>
@@ -284,6 +451,7 @@ const SalesOrderTable = () => {
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
deliveryClickHandler={deliveryClickHandler}
/>
</RowDropdownOptions>
)}
@@ -294,6 +462,7 @@ const SalesOrderTable = () => {
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
deliveryClickHandler={deliveryClickHandler}
/>
</RowCollapseOptions>
)}
@@ -330,16 +499,45 @@ const SalesOrderTable = () => {
}}
/>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={confirmationModal.ref}
type={approveAction === 'approve' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${approveAction} data penjualan (${selectedRowIds.length} data)?`}
type={approveAction === 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan (${idsToProcess.length} data)?`}
secondaryButton={{
text: 'Tidak',
onClick: confirmationModal.closeModal,
}}
primaryButton={{
text: 'Ya',
color: approveAction === 'APPROVED' ? 'success' : 'error',
onClick: approveMarketingHandler,
}}
/>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
secondaryButton={{
text: 'Tidak',
onClick: deleteModal.closeModal,
}}
primaryButton={{
text: 'Ya',
color: 'error',
onClick: deleteMarketingHandler,
}}
/>
<ConfirmationModalWithNotes
ref={deliveryModal.ref}
type={'success'}
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: approveAction === 'approve' ? 'success' : 'error',
color: 'success',
onClick: confirmationModalDeliveryClickHandler,
}}
/>
@@ -361,10 +559,10 @@ const SalesOrderTable = () => {
<Icon icon='mdi:close' width={16} height={16} />
</Button>
</div>
<Table<MarketingProduct>
<Table<BaseSalesOrder>
data={
isResponseSuccess(marketing) && selectedItem
? (selectedItem?.marketing_products ?? [])
? (selectedItem?.sales_order ?? [])
: []
}
columns={[
@@ -403,4 +601,4 @@ const SalesOrderTable = () => {
</>
);
};
export default SalesOrderTable;
export default MarketingTable;
@@ -0,0 +1,461 @@
'use client';
import Button from '@/components/Button';
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import ApprovalSteps, {
useApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import Table from '@/components/Table';
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
import {
cn,
formatCurrency,
formatDate,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import {
MarketingApi,
SalesOrderApi,
} from '@/services/api/marketing/marketing';
import {
BaseDelivery,
BaseSalesOrder,
Marketing,
} from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import toast from 'react-hot-toast';
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
import DeliveryOrderExport from '../pdf/DeliveryOrderExport';
const MarketingDetail = ({
initialValues,
refresh,
}: {
initialValues?: Marketing;
refresh?: () => void;
}) => {
const router = useRouter();
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
'APPROVED'
);
const [grandTotal, setGrandTotal] = useState(
initialValues?.sales_order
?.map((item) => item.total_price)
.reduce((a, b) => a + b, 0)
);
const [isLoading, setIsLoading] = useState(false);
const deleteModal = useModal();
const confirmationModal = useModal();
const deliveryModal = useModal();
const {
approvals,
isLoading: isLoadingApproval,
refresh: refreshApproval,
} = useApprovalSteps({
latestApproval: initialValues?.latest_approval,
approvalLines: MARKETING_APPROVAL_LINE,
moduleName: 'MARKETINGS',
moduleId: initialValues?.id as number as unknown as string,
});
const approveClickHandler = () => {
setApprovalAction('APPROVED');
confirmationModal.openModal();
};
// const rejectClickHandler = () => {
// setApprovalAction('REJECTED');
// confirmationModal.openModal();
// };
const deliveryClickHandler = () => {
deliveryModal.openModal();
};
const deleteClickHandler = () => {
deleteModal.openModal();
};
const confirmationModalDeleteClickHandler = async () => {
setIsLoading(true);
const res = await MarketingApi.delete(initialValues?.id as number);
setIsLoading(false);
deleteModal.closeModal();
toast.success(res?.message as string);
refresh?.();
};
const confirmationModalApproveClickHandler = async (notes: string) => {
setIsLoading(true);
const res = await SalesOrderApi.singleApproval(
initialValues?.id as number,
approvalAction,
notes
);
setIsLoading(false);
confirmationModal.closeModal();
toast.success(res?.message as string);
refresh?.();
refreshApproval?.();
};
const confirmationModalDeliveryClickHandler = async (notes: string) => {
setIsLoading(true);
const res = await SalesOrderApi.delivery(
initialValues?.id as number,
notes
);
setIsLoading(false);
deliveryModal.closeModal();
toast.success(res?.message as string);
refresh?.();
refreshApproval?.();
router.push(
`/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
);
};
return (
<>
<div className='flex flex-col w-full gap-4'>
<FormHeader title='Detail Sales Order' backUrl='/marketing' />
{!isLoadingApproval && approvals && (
<ApprovalSteps approvals={approvals} />
)}
<div className='flex-row flex gap-3'>
{initialValues?.latest_approval?.step_number != 3 && (
<>
<Button
color='success'
onClick={approveClickHandler}
disabled={initialValues?.latest_approval?.step_number != 1}
>
<Icon icon='mdi:check' width={24} height={24} />
Approve
</Button>
{/* <Button
color='error'
onClick={rejectClickHandler}
disabled={initialValues?.latest_approval?.step_number != 2}
>
<Icon icon='mdi:close' width={24} height={24} />
Reject
</Button> */}
</>
)}
{initialValues?.latest_approval?.step_number == 2 && (
<Button
color='success'
href={`/marketing/add/delivery-orders?marketingId=${initialValues?.id}`}
>
<Icon icon='mdi:truck' width={24} height={24} />
Delivery Order
</Button>
)}
</div>
<Card
title='Informasi Penjualan'
className={{
wrapper: 'w-full bg-white',
}}
>
<div className='overflow-x-auto rounded-box border border-base-content/5 bg-base-100 mt-3'>
<table className='table'>
<tbody>
<tr>
<td width='45%' className='font-semibold'>
No. Sales Order
</td>
<td>:</td>
<td width='50%'>{initialValues?.so_number}</td>
</tr>
<tr>
<td className='font-semibold'>Nama Pelanggan</td>
<td>:</td>
<td>{initialValues?.customer?.name}</td>
</tr>
<tr>
<td className='font-semibold'>Status</td>
<td>:</td>
<td>{initialValues?.latest_approval?.step_name}</td>
</tr>
<tr>
<td className='font-semibold'>Tanggal Penjualan</td>
<td>:</td>
<td>{formatDate(initialValues?.so_date, 'DD MMM yyyy')}</td>
</tr>
<tr>
<td className='font-semibold'>Total Penjualan</td>
<td>:</td>
<td>{formatCurrency(grandTotal as number)}</td>
</tr>
<tr>
<td className='font-semibold'>Catatan</td>
<td>:</td>
<td>{initialValues?.notes ?? '-'}</td>
</tr>
<tr>
<td className='font-semibold'>Dokumen</td>
<td>:</td>
<td>
<SalesOrderExport data={initialValues} />
</td>
</tr>
</tbody>
</table>
</div>
</Card>
{initialValues?.sales_order && (
<Card
title='Informasi Produk'
className={{
wrapper: 'w-full bg-white',
}}
>
<Table<BaseSalesOrder>
data={initialValues?.sales_order}
columns={[
{
header: 'Kandang',
accessorFn(row) {
return row.product_warehouse.warehouse.name;
},
},
{
header: 'Produk',
accessorFn(row) {
return row.product_warehouse.product.name;
},
},
{
header: 'Harga Satuan (Rp)',
accessorFn(row) {
return formatCurrency(row.unit_price);
},
},
{
header: 'Total Bobot (Kg)',
accessorFn(row) {
return formatNumber(row.total_weight);
},
},
{
header: 'Kuantitas',
accessorFn(row) {
return formatNumber(row.qty);
},
},
{
header: 'Avg. Bobot (Kg)',
accessorFn(row) {
return formatNumber(row.avg_weight);
},
},
{
header: 'Total Penjualan (Rp)',
accessorFn(row) {
return formatCurrency(row.total_price);
},
},
]}
className={{
containerClassName: cn({
'mb-20':
initialValues?.sales_order &&
initialValues?.sales_order?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
/>
</Card>
)}
{initialValues?.delivery_order && (
<Card
title='Informasi Pengiriman'
className={{
wrapper: 'w-full bg-white',
}}
>
{initialValues?.delivery_order.map((delivery, index) => {
return (
<div key={index}>
<Card
className={{
wrapper: 'w-full',
}}
>
<div className='flex flex-row gap-3'>
<div className='font-semibold'>
Nomor DO : {delivery.do_number}
</div>
</div>
<Table<BaseDelivery>
data={delivery.deliveries}
columns={[
{
header: 'Tanggal Pengiriman',
accessorFn() {
return formatDate(
delivery.delivery_date,
'DD MMM yyyy'
);
},
},
{
header: 'No. Polisi',
accessorFn(row) {
return formatVechicleNumber(row.vehicle_number);
},
},
{
header: 'Kandang',
accessorFn(row) {
return row.product_warehouse.warehouse.name;
},
},
{
header: 'Produk',
accessorFn(row) {
return row.product_warehouse.product.name;
},
},
{
header: 'Harga Satuan (Rp)',
accessorFn(row) {
return formatCurrency(row.unit_price);
},
},
{
header: 'Total Bobot (Kg)',
accessorFn(row) {
return formatNumber(row.total_weight);
},
},
{
header: 'Kuantitas',
accessorFn(row) {
return formatNumber(row.qty);
},
},
{
header: 'Avg. Bobot (Kg)',
accessorFn(row) {
return formatNumber(row.avg_weight);
},
},
{
header: 'Total Penjualan (Rp)',
accessorFn(row) {
return formatCurrency(row.total_price);
},
},
]}
className={{
containerClassName: cn({
'mb-20':
initialValues?.sales_order &&
initialValues?.sales_order?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName:
'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
/>
</Card>
<div className='flex flex-row gap-3 my-3'>
<DeliveryOrderExport
data={initialValues}
deliveryOrder={delivery}
/>
</div>
</div>
);
})}
</Card>
)}
<div className='flex flex-row gap-3'>
<Button
color='warning'
type='button'
href={`/marketing/detail/${initialValues?.latest_approval.step_number == 3 ? 'delivery-orders' : 'sales-orders'}/edit?marketingId=${initialValues?.id}`}
>
<Icon icon='mdi:pencil' width={24} height={24} />
Edit
</Button>
<Button color='error' onClick={deleteClickHandler}>
<Icon icon='mdi:delete' width={24} height={24} />
Hapus
</Button>
</div>
</div>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
<ConfirmationModalWithNotes
ref={confirmationModal.ref}
type={approvalAction === 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: approvalAction === 'APPROVED' ? 'success' : 'error',
isLoading: isLoading,
onClick: confirmationModalApproveClickHandler,
}}
/>
<ConfirmationModalWithNotes
ref={deliveryModal.ref}
type={'success'}
text={`Apakah anda yakin ingin deliver penjualan ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'success',
isLoading: isLoading,
onClick: confirmationModalDeliveryClickHandler,
}}
/>
</>
);
};
export default MarketingDetail;
@@ -0,0 +1,76 @@
import * as Yup from 'yup';
import {
SalesOrderProductFormValues,
SalesOrderProductSchema,
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import {
DeliveryOrderProductFormValues,
DeliveryOrderProductSchema,
} from './repeater/delivery-order/DeliverOrderProduct.schema';
type MarketingSchemaType = {
customer_id: number | undefined;
sales_person_id: number | undefined;
customer:
| {
value: number;
label: string;
}
| undefined
| null;
so_date: string | undefined;
notes: string | undefined;
};
type SalesOrderSchemaType = MarketingSchemaType & {
sales_order: SalesOrderProductFormValues[];
};
type DeliveryOrderSchemaType = {
delivery_order: DeliveryOrderProductFormValues[];
};
export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
Yup.object({
customer_id: Yup.number().required('Customer wajib diisi!'),
sales_person_id: Yup.number().required('Sales Person wajib diisi!'),
customer: Yup.object({
value: Yup.number().required(),
label: Yup.string().required(),
}).nullable(),
so_date: Yup.string().required('Tanggal wajib diisi!'),
notes: Yup.string().required('Catatan wajib diisi!'),
sales_order: Yup.array()
.of(SalesOrderProductSchema)
.min(1, 'Produk wajib diisi!')
.required('Produk wajib diisi!'),
});
export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
Yup.object({
delivery_order: Yup.array()
.of(DeliveryOrderProductSchema)
.min(1, 'Pengiriman wajib diisi!')
.required()
.test(
'at-least-one-delivery-date',
'Minimal ada satu tanggal pengiriman yang harus diisi!',
(value) => {
if (!value || value.length == 0) {
return false;
}
return value.some(
(item) =>
item.delivery_date !== null &&
item.delivery_date !== undefined &&
item.delivery_date !== ''
);
}
),
});
export const UpdateSalesOrderSchema = SalesOrderSchema;
export type SalesOrderFormValues = Yup.InferType<typeof SalesOrderSchema>;
export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
@@ -0,0 +1,757 @@
'use client';
import Button from '@/components/Button';
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import DateInput from '@/components/input/DateInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import TextArea from '@/components/input/TextArea';
import Modal, { useModal } from '@/components/Modal';
import { formatCurrency, formatDate } from '@/lib/helper';
import {
BaseDeliveryOrder,
BaseSalesOrder,
CreateDeliveryOrderPayload,
CreateSalesOrderPayload,
CreateSalesOrderProductPayload,
Marketing,
UpdateDeliveryOrderPayload,
UpdateSalesOrderPayload,
} from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { Customer } from '@/types/api/master-data/customer';
import { CustomerApi } from '@/services/api/master-data';
import { useFormik } from 'formik';
import {
DeliveryOrderFormValues,
DeliveryOrderSchema,
SalesOrderFormValues,
SalesOrderSchema,
} from './MarketingForm.schema';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import {
DeliveryOrderApi,
MarketingApi,
SalesOrderApi,
} from '@/services/api/marketing/marketing';
import { SalesOrderProductFormValues } from './repeater/sales-order/SalesOrderProduct.schema';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import SalesOrderProductTable from './table-view/SalesOrderProductTable';
import SalesOrderProductForm from './repeater/sales-order/SalesOrderProductForm';
import DeliveryOrderProductTable from './table-view/DeliveryOrderProductTable';
import DeliveryOrderProductForm from './repeater/delivery-order/DeliverOrderProduct';
import { DeliveryOrderProductFormValues } from './repeater/delivery-order/DeliverOrderProduct.schema';
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable);
const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm);
const MarketingProductToFieldValues = (
product: BaseSalesOrder
): SalesOrderProductFormValues => {
return {
id: product.id,
vehicle_number: product.vehicle_number,
kandang_id: product.product_warehouse.warehouse.id,
kandang: {
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
},
product_warehouse: {
value: product.product_warehouse.id,
label: product.product_warehouse.product.name,
},
product_warehouse_id: product.product_warehouse.id,
unit_price: product.unit_price,
total_weight: product.total_weight,
qty: product.qty,
avg_weight: product.avg_weight,
total_price: product.total_price,
};
};
const DeliveryProductToFieldValues = (
salesOrders: BaseSalesOrder[],
delivery: BaseDeliveryOrder
): DeliveryOrderProductFormValues[] => {
const data = delivery.deliveries.map((item) => {
const soId = salesOrders.find(
(so) => so.product_warehouse.id === item.product_warehouse.id
)?.id;
return {
id: soId,
unit_price: item.unit_price,
total_weight: item.total_weight,
qty: item.qty,
avg_weight: item.avg_weight,
total_price: item.total_price,
vehicle_number: item.vehicle_number,
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
do_number: delivery.do_number,
marketing_product_id: soId,
marketing_product: {
id: soId,
vehicle_number: item.vehicle_number,
kandang_id: item.product_warehouse.warehouse.id,
kandang: {
value: item.product_warehouse.warehouse.id,
label: item.product_warehouse.warehouse.name,
},
product_warehouse: {
value: item.product_warehouse.id,
label: item.product_warehouse.product.name,
},
product_warehouse_id: item.product_warehouse.id,
unit_price: item.unit_price,
total_weight: item.total_weight,
qty: item.qty,
avg_weight: item.avg_weight,
total_price: item.total_price,
},
} as DeliveryOrderProductFormValues;
});
return data;
};
const mergeSOwithDO = (
salesOrders: SalesOrderProductFormValues[],
deliveryOrders: DeliveryOrderProductFormValues[]
): DeliveryOrderProductFormValues[] => {
return salesOrders.map((so) => {
const delivery = deliveryOrders.find(
(d) => d?.marketing_product_id === so.id
);
return {
...so, // nilai dasar dari sales order
marketing_product_id: so.id,
delivery_date: delivery?.delivery_date || undefined,
do_number: delivery?.do_number || undefined,
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
unit_price: delivery?.unit_price ?? so.unit_price,
total_weight: delivery?.total_weight ?? so.total_weight,
qty: delivery?.qty ?? so.qty,
avg_weight: delivery?.avg_weight ?? so.avg_weight,
total_price: delivery?.total_price ?? so.total_price,
marketing_product: so, // jika ada, override
} as DeliveryOrderProductFormValues;
});
};
const MarketingForm = ({
formType = 'add',
initialValues,
afterSubmit,
}: {
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
initialValues?: Marketing;
afterSubmit?: () => void;
}) => {
const router = useRouter();
const deleteModal = useModal();
const [isLoading, setIsLoading] = useState(false);
const [selectedMarketingProduct, setSelectedMarketingProduct] =
useState<SalesOrderProductFormValues | null>(null);
const [selectedDeliveryProduct, setSelectedDeliveryProduct] =
useState<DeliveryOrderProductFormValues | null>(null);
const [deliveryOrderValues, setDeliveryOrderValues] = useState<
DeliveryOrderProductFormValues[]
>(
mergeSOwithDO(
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
initialValues?.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
) ?? []
)
);
// Repeater Props
const addSOModal = useModal();
const addDOModal = useModal();
const [rowSOSelection, setRowSOSelection] = useState<Record<string, boolean>>(
{}
);
const selectedRowSOIds = Object.keys(rowSOSelection).map((item) =>
parseInt(item)
);
const [rowDOSelection, setRowDOSelection] = useState<Record<string, boolean>>(
{}
);
const selectedRowDOIds = Object.keys(rowDOSelection).map((item) =>
parseInt(item)
);
// End Repeater Props
const {
options: customerOptions,
isLoadingOptions: isLoadingCustomerOptions,
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
const formikInitialValues = useMemo<
SalesOrderFormValues & DeliveryOrderFormValues
>(() => {
return {
so_date: initialValues?.so_date || undefined,
notes: initialValues?.notes || undefined,
customer_id: initialValues?.customer?.id || undefined,
sales_person_id: initialValues?.sales_person?.id || 1,
customer: initialValues?.customer
? {
value: initialValues.customer.id,
label: initialValues.customer.name,
}
: null,
sales_order:
initialValues?.sales_order?.map((product) =>
MarketingProductToFieldValues(product)
) ?? [],
delivery_order: mergeSOwithDO(
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
initialValues?.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
) ?? []
),
};
}, [initialValues]);
const formik = useFormik<SalesOrderFormValues & DeliveryOrderFormValues>({
enableReinitialize: true,
initialValues: formikInitialValues,
validationSchema:
formType == 'add_deliver' || formType == 'edit_deliver'
? DeliveryOrderSchema
: SalesOrderSchema,
validateOnMount: true,
onSubmit: async (values) => {
const payload =
formType != 'add_deliver' && formType != 'edit_deliver'
? ({
customer_id: values.customer_id as number,
sales_person_id: values.sales_person_id as number,
date: formatDate(values.so_date as string, 'yyyy-MM-DD'),
notes: values.notes as string,
marketing_products: values.sales_order.map((product) => {
return {
vehicle_number: product.vehicle_number as string,
kandang_id: product.kandang_id as number,
product_warehouse_id: product.product_warehouse_id as number,
unit_price: parseFloat(product.unit_price as string),
total_weight: parseFloat(product.total_weight as string),
qty: parseFloat(product.qty as string),
avg_weight: parseFloat(product.avg_weight as string),
total_price: parseFloat(product.total_price as string),
} as CreateSalesOrderProductPayload;
}),
} as CreateSalesOrderPayload)
: ({
marketing_id: initialValues?.id as number,
delivery_products: values.delivery_order
.map((product) => {
if (Boolean(product.delivery_date)) {
return {
marketing_product_id:
product.marketing_product_id as number,
unit_price: parseFloat(product.unit_price as string),
total_weight: parseFloat(product.total_weight as string),
qty: parseFloat(product.qty as string),
avg_weight: parseFloat(product.avg_weight as string),
total_price: parseFloat(product.total_price as string),
delivery_date: formatDate(
product.delivery_date as string,
'yyyy-MM-DD'
),
vehicle_number: product.vehicle_number,
};
}
})
.filter((item) => Boolean(item)),
} as UpdateDeliveryOrderPayload);
console.log('PAYLOAD');
console.log(payload);
switch (formType) {
case 'add':
await createMarketingHandler(payload as CreateSalesOrderPayload);
break;
case 'edit':
await updateMarketingHandler(payload as UpdateSalesOrderPayload);
break;
case 'add_deliver':
await createDeliveryHandler(payload as CreateDeliveryOrderPayload);
break;
case 'edit_deliver':
await updateDeliveryHandler(payload as UpdateDeliveryOrderPayload);
break;
default:
break;
}
afterSubmit?.();
},
});
const grandTotal = useMemo(() => {
return formik.values.sales_order.reduce(
(total, product) =>
total + parseFloat((product.total_price as string) || '0'),
0
);
}, [formik.values.sales_order]);
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
setIsLoading(true);
console.log(values);
const createMarketingRes = await SalesOrderApi.create(values);
if (isResponseSuccess(createMarketingRes)) {
toast.success(createMarketingRes?.message as string);
router.push('/marketing');
}
if (isResponseError(createMarketingRes)) {
toast.error(createMarketingRes?.message as string);
}
setIsLoading(false);
};
const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => {
setIsLoading(true);
console.log(values);
const updateMarketingRes = await SalesOrderApi.update(
initialValues?.id as number,
values
);
if (isResponseSuccess(updateMarketingRes)) {
toast.success(updateMarketingRes?.message as string);
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
}
if (isResponseError(updateMarketingRes)) {
toast.error(updateMarketingRes?.message as string);
}
setIsLoading(false);
};
const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => {
setIsLoading(true);
console.log(initialValues?.id);
const createDeliveryRes = await DeliveryOrderApi.create(values);
if (isResponseSuccess(createDeliveryRes)) {
console.log(createDeliveryRes);
toast.success(createDeliveryRes?.message as string);
setDeliveryOrderValues(
createDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(
createDeliveryRes.data?.sales_order,
delivery
)
) ?? []
);
router.push(
`/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
);
}
if (isResponseError(createDeliveryRes)) {
console.log(createDeliveryRes);
toast.error(createDeliveryRes?.message as string);
}
setIsLoading(false);
};
const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
setIsLoading(true);
console.log(initialValues?.id);
const updateDeliveryRes = await DeliveryOrderApi.update(
initialValues?.id as number,
values
);
if (isResponseSuccess(updateDeliveryRes)) {
console.log(updateDeliveryRes);
toast.success(updateDeliveryRes?.message as string);
// router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
setDeliveryOrderValues(
updateDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(
updateDeliveryRes.data?.sales_order,
delivery
)
) ?? []
);
}
if (isResponseError(updateDeliveryRes)) {
console.log(updateDeliveryRes);
toast.error(updateDeliveryRes?.message as string);
}
setIsLoading(false);
};
const deleteMarketingHandler = async () => {
setIsLoading(true);
console.log(initialValues?.id);
const deleteMarketingRes = await MarketingApi.delete(
initialValues?.id as number
);
if (isResponseSuccess(deleteMarketingRes)) {
console.log(deleteMarketingRes);
toast.success(deleteMarketingRes?.message as string);
}
if (isResponseError(deleteMarketingRes)) {
console.log(deleteMarketingRes);
toast.error(deleteMarketingRes?.message as string);
}
setIsLoading(false);
deleteModal.closeModal();
router.push('/marketing/sales-orders');
};
const handleChangeCustomer = useCallback(
(val: OptionType | OptionType[] | null) => {
formik.setFieldValue('customer_id', (val as OptionType)?.value);
formik.setFieldValue('customer', val as OptionType);
},
[formik]
);
// Repeater Handle
const handleDeleteSO = useCallback(
(id: number) => {
const currentProducts = formik.values.sales_order;
formik.setFieldValue(
'sales_order',
currentProducts.filter((p) => p.id != id)
);
},
[formik]
);
const handleBulkDeleteSO = useCallback(() => {
const currentProducts = formik.values.sales_order;
formik.setFieldValue(
'sales_order',
currentProducts.filter(
(product) => !selectedRowSOIds.includes(product.id ?? -1)
)
);
setRowSOSelection({});
}, [formik, selectedRowSOIds]);
const handleDelete = useCallback(() => {
deleteModal.openModal();
}, [deleteModal]);
const handleAddSOClick = useCallback(() => {
setSelectedMarketingProduct(null);
addSOModal.openModal();
}, [addSOModal]);
const handleAddSubmitSO = useCallback(
async (values: SalesOrderProductFormValues) => {
const currentProducts = formik.values.sales_order;
const newValues = {
...values,
id: values.id ?? Date.now(),
};
formik.setFieldValue('sales_order', [...currentProducts, newValues]);
addSOModal.closeModal();
},
[formik, addSOModal]
);
const handleDeleteDO = useCallback(
(id: number) => {
const currentProducts = formik.values.delivery_order;
setDeliveryOrderValues((prev) => prev.filter((p) => p.id !== id));
},
[formik]
);
const handleEditDO = useCallback(
(id: number) => {
const currentProducts = formik.values.delivery_order.find(
(product) => product.id == id
);
setSelectedDeliveryProduct(currentProducts ?? null);
addDOModal.openModal();
},
[formik]
);
const handleBulkDeleteDO = useCallback(() => {
setDeliveryOrderValues((prev) =>
prev.filter((product) => !selectedRowDOIds.includes(product.id ?? -1))
);
setRowDOSelection({});
}, [formik, selectedRowDOIds]);
const handleAddDOClick = useCallback(() => {
setSelectedDeliveryProduct(null);
addDOModal.openModal();
}, [addDOModal]);
const handleAddSubmitDO = useCallback(
async (values: DeliveryOrderProductFormValues) => {
const newValues = {
...values,
id: values.id ?? Date.now(),
};
setDeliveryOrderValues((prev) => [...prev, newValues]);
addDOModal.closeModal();
},
[formik, addDOModal]
);
const handleInputDate = useCallback(
(newData: DeliveryOrderProductFormValues) => {
setDeliveryOrderValues((prev) => {
return prev.map((item) => {
if (item.marketing_product_id == newData.marketing_product_id) {
return newData;
}
return item;
});
});
},
[]
);
const handleUpdateDO = useCallback(
async (id: number, values: DeliveryOrderProductFormValues) => {
setDeliveryOrderValues((prev) =>
prev.map((product) =>
product.id === id ? { ...product, ...values } : product
)
);
setSelectedDeliveryProduct(null);
addDOModal.closeModal();
},
[formik, addDOModal]
);
// End Repeater Handle
const memoSalesOrder = formik.values.sales_order;
useEffect(() => {
formik.setFieldValue('delivery_order', deliveryOrderValues);
}, [deliveryOrderValues, initialValues]);
return (
<>
<form
className='flex flex-col gap-4'
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
>
<FormHeader
title={`${formType == 'add' || formType == 'add_deliver' ? 'Tambah' : 'Edit'} ${formType === 'add_deliver' || formType === 'edit_deliver' ? 'Delivery' : 'Sales'} Order`}
backUrl='/marketing'
/>
<Card
title='Informasi Order'
className={{
wrapper: 'bg-white w-full',
}}
>
<div className='grid grid-cols-2 gap-3 mt-3'>
<SelectInput
label='Pelanggan'
options={customerOptions}
isLoading={isLoadingCustomerOptions}
value={formik.values.customer}
onChange={handleChangeCustomer}
isError={
formik.touched.customer_id && Boolean(formik.errors.customer_id)
}
errorMessage={formik.errors.customer_id}
isClearable
placeholder='Pilih Pelanggan'
isDisabled={
formType === 'add_deliver' || formType === 'edit_deliver'
}
/>
<DateInput
name='so_date'
label='Tanggal'
value={formik.values.so_date}
onChange={formik.handleChange}
isError={formik.touched.so_date && Boolean(formik.errors.so_date)}
errorMessage={formik.errors.so_date}
placeholder='Pilih Tanggal'
readOnly={formType == 'add_deliver' || formType == 'edit_deliver'}
/>
</div>
</Card>
{(formType == 'add' || formType == 'edit') && (
<Card
title='Informasi Produk'
className={{
wrapper: 'bg-white w-full',
}}
>
{/* <div className='text-blue-500'>{JSON.stringify(initialValues)}</div>
<div className='text-green-500'>{JSON.stringify(formik.values)}</div>
<div className='text-red-500'>{JSON.stringify(formik.errors)}</div> */}
<MemoizedSalesOrderProductTable
formType={formType}
data={memoSalesOrder}
rowSelection={rowSOSelection}
setRowSelection={setRowSOSelection}
selectedRowIds={selectedRowSOIds}
onDelete={handleDeleteSO}
onBulkDelete={handleBulkDeleteSO}
onAddProductClick={handleAddSOClick}
/>
</Card>
)}
{(formType == 'add_deliver' || formType == 'edit_deliver') &&
initialValues?.sales_order &&
initialValues?.sales_order.length > 0 && (
<Card
title='Informasi Pengiriman'
className={{
wrapper: 'bg-white w-full',
}}
>
{/* {JSON.stringify(memoSalesOrder)} */}
{/* <small>{JSON.stringify(memoDeliveryOrder)}</small> */}
{/* <small className='block text-error'>
{JSON.stringify(formik.errors)}
</small> */}
<MemoizedDeliveryOrderProductTable
formType={formType}
data={deliveryOrderValues}
salesOrder={memoSalesOrder}
rowSelection={rowDOSelection}
setRowSelection={setRowDOSelection}
selectedRowIds={selectedRowDOIds}
onDelete={handleDeleteDO}
onEdit={handleEditDO}
onBulkDelete={handleBulkDeleteDO}
onAddProductClick={handleAddDOClick}
onInputDate={handleInputDate}
/>
</Card>
)}
<div className='grid grid-cols-2 gap-3'>
<TextArea
required
name='notes'
label='Catatan'
rows={3}
placeholder='Masukan catatan penjualan'
value={formik.values.notes}
onChange={formik.handleChange}
isError={formik.touched.notes && Boolean(formik.errors.notes)}
errorMessage={formik.errors.notes}
disabled={formType === 'add_deliver' || formType === 'edit_deliver'}
/>
<div className='flex flex-col h-full justify-between items-end py-6'>
<span>Total Penjualan</span>
<span className='text-lg font-semibold'>
{formatCurrency(grandTotal)}{' '}
</span>
</div>
</div>
<div className='flex flex-row items-start justify-center gap-2 mt-4'>
<Button type='reset' color='warning' disabled={formik.isSubmitting}>
Reset
</Button>
<Button
type='submit'
disabled={!formik.isValid || formik.isSubmitting}
isLoading={formik.isSubmitting}
>
Submit
</Button>
</div>
</form>
{formType == 'edit' && (
<div className='flex flex-row justify-start'>
<Button
type='button'
color='error'
onClick={handleDelete}
isLoading={isLoading}
>
<Icon icon='mdi:trash' width={24} height={24} />
Hapus
</Button>
</div>
)}
<Modal
ref={addSOModal.ref}
closeOnBackdrop
className={{
modalBox: 'max-w-4/5 z-100',
}}
>
<div className='flex flex-col gap-4'>
<div className='flex flex-row items-center justify-between'>
<h3 className='text-lg font-semibold mb-4'>Tambah Produk</h3>
<Button
variant='ghost'
color='error'
className='rounded-full'
onClick={addSOModal.closeModal}
>
<Icon icon='mdi:close' width={20} height={20} />
</Button>
</div>
<div>
<MemoizedSalesOrderProductForm
onSubmitForm={handleAddSubmitSO}
initialValues={selectedMarketingProduct ?? undefined}
/>
</div>
</div>
</Modal>
<Modal
ref={addDOModal.ref}
closeOnBackdrop
className={{
modalBox: 'max-w-4/5 z-100',
}}
>
<div className='flex flex-col gap-4'>
<div className='flex flex-row items-center justify-between'>
<h3 className='text-lg font-semibold mb-4'>
{selectedDeliveryProduct ? 'Edit' : 'Tambah'} Pengiriman
</h3>
<Button
variant='ghost'
color='error'
className='rounded-full'
onClick={addDOModal.closeModal}
>
<Icon icon='mdi:close' width={20} height={20} />
</Button>
</div>
<div>
<MemoizedDeliveryOrderProductForm
salesOrders={initialValues?.sales_order ?? []}
onSubmitForm={handleAddSubmitDO}
initialValues={selectedDeliveryProduct ?? undefined}
onUpdateForm={handleUpdateDO}
/>
</div>
</div>
</Modal>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
secondaryButton={{
text: 'Tidak',
onClick: deleteModal.closeModal,
}}
primaryButton={{
text: 'Ya',
color: 'error',
onClick: deleteMarketingHandler,
}}
/>
</>
);
};
export default MarketingForm;
@@ -0,0 +1,63 @@
import * as Yup from 'yup';
import {
SalesOrderProductFormValues,
SalesOrderProductSchema,
} from '../sales-order/SalesOrderProduct.schema';
import { de } from 'react-day-picker/locale';
type DeliveryOrderProductSchemaType = {
id?: number | undefined;
marketing_product_id: number | undefined; // Sales Order ID
marketing_product?: SalesOrderProductFormValues | undefined | null;
unit_price: string | number | undefined;
total_weight: string | number | undefined;
qty: string | number | undefined;
avg_weight: string | number | undefined;
total_price: string | number | undefined;
vehicle_number: string | undefined;
delivery_date: string | undefined | null;
do_number?: string | undefined | null; // Uncertain
};
export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSchemaType> =
Yup.object({
id: Yup.number(),
marketing_product_id: Yup.number()
.min(1, 'Produk wajib diisi!')
.required('Produk wajib diisi!'),
marketing_product: Yup.object().nullable().optional(),
unit_price: Yup.number()
.min(1, 'Harga Satuan wajib diisi!')
.required('Harga Satuan wajib diisi!'),
total_weight: Yup.number()
.min(0, 'Total Bobot wajib diisi!')
.required('Total Bobot wajib diisi!'),
qty: Yup.number()
.min(1, 'Kuantitas wajib diisi!')
.required('Kuantitas wajib diisi!'),
avg_weight: Yup.number()
.min(0, 'Avg. Bobot wajib diisi!')
.required('Avg. Bobot wajib diisi!'),
total_price: Yup.number()
.min(1, 'Total Penjualan wajib diisi!')
.required('Total Penjualan wajib diisi!'),
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
delivery_date: Yup.string()
.required('Tanggal Pengiriman wajib diisi!')
.nullable()
.optional(),
do_number: Yup.string().nullable().optional(),
});
export type DeliveryOrderProductFormValues = Yup.InferType<
typeof DeliveryOrderProductSchema
>;
// "marketing_product_id": 3,
// "qty": 20,
// "unit_price": 1000,
// "avg_weight": 1.1,
// "total_weight": 220,
// "total_price": 20000,
// "delivery_date": "2025-11-09",
// "vehicle_number": "D 4321 XXX"
@@ -0,0 +1,354 @@
import { useEffect, useState } from 'react';
import {
DeliveryOrderProductFormValues,
DeliveryOrderProductSchema,
} from './DeliverOrderProduct.schema';
import { useFormik } from 'formik';
import Alert from '@/components/Alert';
import Button from '@/components/Button';
import NumberInput from '@/components/input/NumberInput';
import PatternInput from '@/components/input/PatternInput';
import { formatVechicleNumber } from '@/lib/helper';
import DateInput from '@/components/input/DateInput';
import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { SalesOrderProductFormValues } from '../sales-order/SalesOrderProduct.schema';
import { BaseSalesOrder } from '@/types/api/marketing/marketing';
import Badge from '@/components/Badge';
const DeliveryOrderProductForm = ({
salesOrders,
initialValues,
onSubmitForm,
onUpdateForm,
}: {
salesOrders: BaseSalesOrder[];
initialValues?: DeliveryOrderProductFormValues;
onSubmitForm?: (value: DeliveryOrderProductFormValues) => Promise<void>;
onUpdateForm?: (
id: number,
value: DeliveryOrderProductFormValues
) => Promise<void>;
}) => {
const [formikErrorMessage, setFormErrorMessage] = useState('');
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
null
);
const formik = useFormik<DeliveryOrderProductFormValues>({
enableReinitialize: true,
initialValues: {
delivery_date: initialValues?.delivery_date || undefined,
vehicle_number: initialValues?.vehicle_number || undefined,
marketing_product_id: initialValues?.marketing_product_id || undefined,
unit_price: initialValues?.unit_price || undefined,
total_weight: initialValues?.total_weight || undefined,
qty: initialValues?.qty || undefined,
avg_weight: initialValues?.avg_weight || undefined,
total_price: initialValues?.total_price || undefined,
marketing_product: initialValues?.marketing_product || undefined,
},
validationSchema: DeliveryOrderProductSchema,
validateOnBlur: true,
validateOnChange: false,
onSubmit: async (values) => {
setFormErrorMessage('');
if (initialValues?.id) {
await onUpdateForm?.(initialValues.id, values);
} else {
await onSubmitForm?.(values);
}
handleResetForm();
},
});
const handleResetForm = () => {
setFormErrorMessage('');
formik.resetForm({
values: {
delivery_date: '',
vehicle_number: '',
marketing_product_id: undefined,
unit_price: '',
total_weight: '',
qty: '',
avg_weight: '',
total_price: '',
marketing_product: undefined,
},
});
setSelectedProduct(null);
};
const handleBlurField = (field: string) => {
const { qty, unit_price, total_price, avg_weight, total_weight } =
formik.values;
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
formik.setFieldValue('total_price', Number(qty) * Number(unit_price));
} else if (qty && total_price && field === 'total_price') {
formik.setFieldValue('unit_price', Number(total_price) / Number(qty));
}
}
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
formik.setFieldValue('total_weight', Number(qty) * Number(avg_weight));
} else if (qty && total_weight && field === 'total_weight') {
formik.setFieldValue('avg_weight', Number(total_weight) / Number(qty));
}
}
};
const MarketingProductToFieldValues = (
product: BaseSalesOrder
): SalesOrderProductFormValues => {
return {
id: product.id,
vehicle_number: product.vehicle_number,
kandang_id: product.product_warehouse.warehouse.id,
kandang: {
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
},
product_warehouse: {
value: product.product_warehouse.id,
label: product.product_warehouse.product.name,
},
product_warehouse_id: product.product_warehouse.id,
unit_price: product.unit_price,
total_weight: product.total_weight,
qty: product.qty,
avg_weight: product.avg_weight,
total_price: product.total_price,
};
};
const options = salesOrders.map((item) => ({
value: item.id,
label: `${item.product_warehouse.product.name} - ${item.product_warehouse.warehouse.name}`,
}));
const { setValues: setFormikValues } = formik;
useEffect(() => {
if (initialValues) {
setFormikValues(initialValues);
const value = salesOrders.find(
(item) => item.id === initialValues.marketing_product_id
);
setSelectedProduct({
value: value?.id,
label: `${value?.product_warehouse.product.name} - ${value?.product_warehouse.warehouse.name}`,
} as OptionType);
}
}, [initialValues]);
return (
<>
<form
className='size-full'
onSubmit={formik.handleSubmit}
onReset={handleResetForm}
>
{/* <small className='block text-blue-500'>
{JSON.stringify(initialValues)}
</small>
<small className='block text-red-500'>
{JSON.stringify(formik.errors)}
</small>
<small className='block text-emerald-500'>
{JSON.stringify(formik.values)}
</small>
<div className='hidden'>
{JSON.stringify(formik.values.marketing_product)}
</div> */}
{formikErrorMessage && (
<div onClick={() => setFormErrorMessage('')} className='my-3 w-full'>
<Alert color='error'>{formikErrorMessage}</Alert>
</div>
)}
<div className='grid grid-cols-2 gap-4'>
<SelectInput
options={options}
label='Produk'
placeholder='Pilih Produk'
isDisabled
value={
selectedProduct
? ({
value: selectedProduct?.value,
label: salesOrders.find(
(item) => item.id === selectedProduct?.value
)?.product_warehouse.product.name,
} as OptionType)
: null
}
onChange={(value) => {
const selected = value as OptionType;
setSelectedProduct(selected);
const so = salesOrders.find(
(item) => item.id === selected?.value
);
if (!so) {
formik.setValues({
...formik.values,
marketing_product_id: undefined,
marketing_product: null,
qty: formik.values.qty || '',
unit_price: '',
total_price: '',
avg_weight: '',
total_weight: '',
vehicle_number: '',
});
return;
}
formik.setValues({
...formik.values,
marketing_product_id: selected.value as number,
marketing_product: MarketingProductToFieldValues(so),
qty: formik.values.qty || so.qty,
unit_price: so.unit_price,
total_price: so.total_price,
avg_weight: so.avg_weight,
total_weight: so.total_weight,
vehicle_number: so.vehicle_number,
});
}}
startAdornment={
selectedProduct && (
<Badge
variant='soft'
color='success'
size='sm'
className={{ badge: 'whitespace-nowrap font-semibold' }}
>
{
salesOrders.find(
(item) => item.id === selectedProduct?.value
)?.product_warehouse?.warehouse?.name
}
</Badge>
)
}
isClearable
isError={Boolean(formik.errors.marketing_product_id)}
errorMessage={formik.errors.marketing_product_id}
required
/>
<DateInput
name='delivery_date'
label='Tanggal'
value={formik.values.delivery_date ?? undefined}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.delivery_date &&
Boolean(formik.errors.delivery_date)
}
errorMessage={formik.errors.delivery_date}
placeholder='Pilih Tanggal'
required
/>
<PatternInput
name='vehicle_number'
label='No. Polisi'
format='AA #### AAA'
mask='_'
inputVehicleNumber
required
type='text'
placeholder='B 1234 CDE'
value={formatVechicleNumber(formik.values.vehicle_number ?? '')}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(formik.errors.vehicle_number)}
errorMessage={formik.errors.vehicle_number}
/>
<NumberInput
required
label='Kuantitas'
name='qty'
value={formik.values.qty}
onChange={formik.handleChange}
onBlur={() => handleBlurField('qty')}
isError={Boolean(formik.errors.qty)}
errorMessage={formik.errors.qty}
placeholder='Masukan Kuantitas'
/>
<NumberInput
required
label='Avg. Bobot (Kg)'
name='avg_weight'
value={formik.values.avg_weight}
onChange={formik.handleChange}
onBlur={() => handleBlurField('avg_weight')}
isError={Boolean(formik.errors.avg_weight)}
errorMessage={formik.errors.avg_weight}
placeholder='Masukan Bobot Rata-rata'
/>
<NumberInput
required
label='Harga Satuan (Rp)'
name='unit_price'
value={formik.values.unit_price}
onChange={formik.handleChange}
onBlur={() => handleBlurField('unit_price')}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
<NumberInput
required
label='Total Bobot (Kg)'
name='total_weight'
value={formik.values.total_weight}
onChange={formik.handleChange}
onBlur={() => handleBlurField('total_weight')}
isError={Boolean(formik.errors.total_weight)}
errorMessage={formik.errors.total_weight}
placeholder='Masukan Total Bobot'
/>
<NumberInput
required
label='Total Penjualan (Rp)'
name='total_price'
value={formik.values.total_price}
onChange={formik.handleChange}
onBlur={() => handleBlurField('total_price')}
isError={Boolean(formik.errors.total_price)}
errorMessage={formik.errors.total_price}
placeholder='Masukan Total Penjualan'
/>
</div>
<div className='flex flex-row justify-end gap-3 mt-4'>
<Button type='reset' color='warning'>
Reset
</Button>
<Button
type='submit'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</div>
</form>
</>
);
};
export default DeliveryOrderProductForm;
@@ -1,7 +1,7 @@
import * as Yup from 'yup';
type MarketingProductSchemaType = {
vehicle_number: string | undefined;
type SalesOrderProductSchemaType = {
id?: number | undefined;
kandang_id?: number;
kandang?: {
value: number;
@@ -15,15 +15,15 @@ type MarketingProductSchemaType = {
unit_price: string | number | undefined;
total_weight: string | number | undefined;
qty: string | number | undefined;
uom: string | undefined | null;
avg_weight: string | number | undefined;
total_price: string | number | undefined;
delivery_date?: string | undefined | null;
vehicle_number?: string | undefined;
};
export const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType> =
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
Yup.object({
vehicle_number: Yup.string().required('No. Polisi wajib diisi!'),
id: Yup.number(),
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
@@ -42,21 +42,19 @@ export const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType
.min(1, 'Harga Satuan wajib diisi!')
.required('Harga Satuan wajib diisi!'),
total_weight: Yup.number()
.min(1, 'Total Bobot wajib diisi!')
.min(0, 'Total Bobot wajib diisi!')
.required('Total Bobot wajib diisi!'),
qty: Yup.number()
.min(1, 'Kuantitas wajib diisi!')
.required('Kuantitas wajib diisi!'),
uom: Yup.string().nullable(),
avg_weight: Yup.number()
.min(1, 'Avg. Bobot wajib diisi!')
.min(0, 'Avg. Bobot wajib diisi!')
.required('Avg. Bobot wajib diisi!'),
total_price: Yup.number()
.min(1, 'Total Penjualan wajib diisi!')
.required('Total Penjualan wajib diisi!'),
delivery_date: Yup.string().required().nullable(),
});
export type MarketingProductFormValues = Yup.InferType<
typeof MarketingProductSchema
export type SalesOrderProductFormValues = Yup.InferType<
typeof SalesOrderProductSchema
>;
@@ -1,17 +1,11 @@
'use client';
import TextInput from '@/components/input/TextInput';
import {
CreateMarketingPayload,
CreateMarketingProductPayload,
MarketingProduct,
} from '@/types/api/marketing/marketing';
import { useFormik } from 'formik';
import {
MarketingProductFormValues,
MarketingProductSchema,
} from './MarketingProduct.schema';
import { RefObject, use, useEffect, useRef, useState } from 'react';
SalesOrderProductFormValues,
SalesOrderProductSchema,
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { RefObject, useState } from 'react';
import SelectInput, {
OptionType,
useSelect,
@@ -23,37 +17,47 @@ import { ProductWarehouseApi } from '@/services/api/inventory';
import NumberInput from '@/components/input/NumberInput';
import Button from '@/components/Button';
import { isResponseSuccess } from '@/lib/api-helper';
import PatternInput from '@/components/input/PatternInput';
import { formatVechicleNumber } from '@/lib/helper';
import PatternInput from '@/components/input/PatternInput';
import Alert from '@/components/Alert';
const MarketingProductForm = ({
const SalesOrderProductForm = ({
initialValues,
data,
modalRef,
onSubmitForm,
}: {
initialValues?: MarketingProduct;
data: MarketingProduct[];
initialValues?: SalesOrderProductFormValues;
modalRef?: RefObject<HTMLDialogElement | null>;
onSubmitForm?: (
tableValues: CreateMarketingProductPayload,
fieldValues: MarketingProductFormValues
) => Promise<void>;
onSubmitForm?: (value: SalesOrderProductFormValues) => Promise<void>;
}) => {
// State
const [selectedOptionsKandang, setSelectedOptionsKandang] =
useState<OptionType | null>(null);
const [selectedOptionsWarehouse, setSelectedOptionsWarehouse] = useState<
OptionType | null | undefined
>(undefined);
const [formErrorMessage, setFormErrorMessage] = useState('');
// Options Data
const formik = useFormik<SalesOrderProductFormValues>({
enableReinitialize: true,
initialValues: {
vehicle_number: initialValues?.vehicle_number || undefined,
kandang_id: initialValues?.kandang_id || undefined,
kandang: initialValues?.kandang || undefined,
product_warehouse: initialValues?.product_warehouse || undefined,
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
unit_price: initialValues?.unit_price || undefined,
total_weight: initialValues?.total_weight || undefined,
qty: initialValues?.qty || undefined,
avg_weight: initialValues?.avg_weight || undefined,
total_price: initialValues?.total_price || undefined,
},
validationSchema: SalesOrderProductSchema,
onSubmit: async (values) => {
setFormErrorMessage('');
onSubmitForm?.(values);
handleResetForm();
},
});
const {
options: kandangSourceOptions,
rawData: kandangSourceRawData,
isLoadingOptions: isLoadingKandangSourceOptions,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const {
options: warehouseSourceOptions,
rawData: warehouseSourceRawData,
@@ -64,109 +68,35 @@ const MarketingProductForm = ({
'product.name',
'search',
{
warehouse_id: selectedOptionsKandang?.value?.toString() ?? '',
warehouse_id: formik.values.kandang_id?.toString() ?? '',
}
);
// Handler
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedOptionsKandang(val as OptionType);
formik.setFieldValue('kandang', val as OptionType);
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('qty', null);
warehouseChangeHandler(null);
};
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedOptionsWarehouse(val as OptionType);
formik.setFieldValue('product_warehouse', val as OptionType);
formik.setFieldValue('product_warehouse_id', (val as OptionType)?.value);
if (isResponseSuccess(warehouseSourceRawData)) {
const newId = (val as OptionType)?.value;
formik.setFieldValue('product_warehouse_id', newId);
if (isResponseSuccess(warehouseSourceRawData) && newId) {
const productWarehouse = warehouseSourceRawData?.data.find(
(item: ProductWarehouse) => item.id === (val as OptionType)?.value
(item: ProductWarehouse) => item.id === newId
);
if (selectedOptionsWarehouse?.value !== null) {
formik.setFieldValue('qty', productWarehouse?.quantity);
handleBlurField('qty');
} else {
formik.setFieldValue('qty', null);
}
formik.setFieldValue('qty', productWarehouse?.quantity);
handleBlurField('qty');
} else {
formik.setFieldValue('qty', null);
}
};
// Formik
const formik = useFormik<MarketingProductFormValues>({
enableReinitialize: true,
initialValues: {
vehicle_number:
initialValues?.marketing_delivery_products?.vehicle_number || undefined,
kandang_id: initialValues?.product_warehouse.warehouse.id || undefined,
kandang: {
value: initialValues?.product_warehouse.warehouse.id as number,
label: initialValues?.product_warehouse.warehouse.name as string,
},
product_warehouse: {
value: initialValues?.product_warehouse.product.id as number,
label: initialValues?.product_warehouse.product.name as string,
},
product_warehouse_id:
initialValues?.product_warehouse.product.id || undefined,
unit_price: initialValues?.unit_price || undefined,
total_weight: initialValues?.total_weight || undefined,
qty: initialValues?.qty || undefined,
uom: initialValues?.product_warehouse?.product?.uom?.name || undefined,
avg_weight: initialValues?.avg_weight || undefined,
total_price: initialValues?.total_price || undefined,
delivery_date:
initialValues?.marketing_delivery_products?.delivery_date ||
new Date().toDateString() ||
undefined,
},
validationSchema: MarketingProductSchema,
onSubmit: async (values) => {
setFormErrorMessage('');
if (
isResponseSuccess(kandangSourceRawData) &&
isResponseSuccess(warehouseSourceRawData)
) {
const productWarehouse = warehouseSourceRawData?.data.find(
(item: ProductWarehouse) => item.id === values.product_warehouse_id
);
const kandang = kandangSourceRawData?.data.find(
(item: Kandang) => item.id === values.kandang_id
);
const marketingProduct: CreateMarketingProductPayload = {
id: initialValues?.id || undefined,
vehicle_number: formatVechicleNumber(values.vehicle_number as string),
kandang_id: values.kandang_id as number,
kandang: kandang,
product_warehouse_id: values.product_warehouse_id as number,
product_warehouse: productWarehouse,
unit_price: values.unit_price as number,
total_weight: values.total_weight as number,
qty: values.qty as number,
uom: values.uom as string,
avg_weight: values.avg_weight as number,
total_price: values.total_price as number,
delivery_date: values.delivery_date as string,
};
onSubmitForm?.(marketingProduct, values);
handleResetForm();
}
},
});
const { setValues: formikSetValues } = formik;
useEffect(() => {
formikSetValues(formik.initialValues);
}, [formikSetValues, formik.initialValues]);
const handleResetForm = () => {
setSelectedOptionsKandang(null);
setSelectedOptionsWarehouse(null);
setFormErrorMessage('');
formik.resetForm({
values: {
@@ -178,10 +108,8 @@ const MarketingProductForm = ({
unit_price: '',
total_weight: '',
qty: '',
uom: '',
avg_weight: '',
total_price: '',
delivery_date: new Date().toDateString(),
},
});
};
@@ -191,7 +119,7 @@ const MarketingProductForm = ({
formik.values;
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
if (qty && unit_price && field === 'unit_price') {
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
formik.setFieldValue(
'total_price',
(qty as number) * (unit_price as number)
@@ -205,7 +133,7 @@ const MarketingProductForm = ({
}
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
if (qty && avg_weight && field === 'avg_weight') {
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
formik.setFieldValue(
'total_weight',
(qty as number) * (avg_weight as number)
@@ -226,6 +154,16 @@ const MarketingProductForm = ({
onSubmit={formik.handleSubmit}
onReset={handleResetForm}
>
{formErrorMessage && (
<div onClick={() => setFormErrorMessage('')} className='my-3 w-full'>
<Alert color='error'>
{formErrorMessage ? formErrorMessage : ''}
</Alert>
</div>
)}
{/* <small className='block text-rose-500'>
{JSON.stringify(formik.errors)}
</small> */}
<div className='grid grid-cols-2 gap-4 z-200'>
<PatternInput
name='vehicle_number'
@@ -250,10 +188,9 @@ const MarketingProductForm = ({
label='Kandang'
options={kandangSourceOptions}
isLoading={isLoadingKandangSourceOptions}
value={selectedOptionsKandang}
value={formik.values.kandang}
onChange={kandangChangeHandler}
isClearable
menuPortalTarget={modalRef?.current}
isError={
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
}
@@ -265,12 +202,11 @@ const MarketingProductForm = ({
label='Produk'
options={warehouseSourceOptions}
isLoading={isLoadingWarehouseSourceOptions}
value={selectedOptionsWarehouse}
value={formik.values.product_warehouse}
onChange={warehouseChangeHandler}
isClearable
menuPortalTarget={modalRef?.current}
placeholder='Pilih Kandang Terlebih Dahulu'
isDisabled={!selectedOptionsKandang?.value}
isDisabled={!formik.values.kandang_id}
isError={
formik.touched.product_warehouse_id &&
Boolean(formik.errors.product_warehouse_id)
@@ -358,4 +294,4 @@ const MarketingProductForm = ({
);
};
export default MarketingProductForm;
export default SalesOrderProductForm;
@@ -0,0 +1,280 @@
import Table from '@/components/Table';
import { DeliveryOrderProductFormValues } from '../repeater/delivery-order/DeliverOrderProduct.schema';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
import * as TanStack from '@tanstack/react-table';
import { useMemo, useRef } from 'react';
import CheckboxInput from '@/components/input/CheckboxInput';
import {
cn,
formatCurrency,
formatDate,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import { SalesOrderProductFormValues } from '../repeater/sales-order/SalesOrderProduct.schema';
import DateInput from '@/components/input/DateInput';
type DeliveryOrderProductTableProps = {
data: DeliveryOrderProductFormValues[];
salesOrder: SalesOrderProductFormValues[];
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
rowSelection: Record<string, boolean>;
setRowSelection: React.Dispatch<
React.SetStateAction<Record<string, boolean>>
>;
selectedRowIds: number[];
onDelete: (id: number) => void;
onEdit: (id: number) => void;
onBulkDelete: () => void;
onAddProductClick: () => void;
onInputDate: (data: DeliveryOrderProductFormValues) => void;
};
const DeliveryOrderProductTable = ({
data,
salesOrder,
formType,
rowSelection,
setRowSelection,
selectedRowIds,
onDelete,
onEdit,
onBulkDelete,
onAddProductClick,
onInputDate,
}: DeliveryOrderProductTableProps) => {
const onDeleteRef = useRef(onDelete);
const onEditRef = useRef(onDelete);
onDeleteRef.current = onDelete;
onEditRef.current = onEdit;
const canAddData = salesOrder.reduce((acc, curr) => {
const deliveredQty = data.filter(
(deliveryItem) => deliveryItem.marketing_product_id == curr.id
);
return acc && deliveredQty.length != salesOrder.length;
}, true);
const columns = useMemo(() => {
const cols = [
// {
// id: 'select',
// header: ({
// table,
// }: {
// table: TanStack.Table<DeliveryOrderProductFormValues>;
// }) => (
// <div className='w-full flex flex-row justify-center'>
// <CheckboxInput
// name='allRow'
// checked={table.getIsAllRowsSelected()}
// indeterminate={table.getIsSomeRowsSelected()}
// onChange={table.getToggleAllRowsSelectedHandler()}
// />
// </div>
// ),
// cell: ({
// row,
// }: {
// row: TanStack.Row<DeliveryOrderProductFormValues>;
// }) => (
// <div>
// <CheckboxInput
// name='row'
// checked={row.getIsSelected()}
// disabled={!row.getCanSelect()}
// indeterminate={row.getIsSomeSelected()}
// onChange={row.getToggleSelectedHandler()}
// />
// </div>
// ),
// },
{
accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number,
header: 'No. Pengiriman',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) => <div>{props.row.original.do_number}</div>,
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
row.delivery_date
? formatDate(row.delivery_date as string, 'DD MMM YYYY')
: '-',
header: 'Tanggal Delivery',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) => (
<>
{formType == 'add_deliver' && (
<DateInput
name={`delivery_date_${props.row.original.marketing_product_id}`}
className={{
input: 'p-0',
inputWrapper: 'py-1 px-3 h-fit w-fit bg-white',
wrapper: 'p-0',
}}
value={
props.row.original.delivery_date
? formatDate(props.row.original.delivery_date, 'yyyy-MM-DD')
: undefined
}
onChange={(val) => {
onInputDate({
...props.row.original,
delivery_date: val.target.value,
});
}}
/>
)}
{formType == 'edit_deliver' &&
formatDate(
props.row.original.delivery_date as string,
'DD MMM YYYY'
)}
</>
),
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
formatVechicleNumber(row.vehicle_number as string),
header: 'No. Polisi',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
row.marketing_product?.kandang?.label,
header: 'Kandang',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
row.marketing_product?.product_warehouse?.label,
header: 'Produk',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
formatCurrency(parseFloat(row.unit_price as string)),
header: 'Harga Satuan (Rp)',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
formatNumber(parseFloat(row.total_weight as string)),
header: 'Total Bobot (Kg)',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
formatNumber(parseFloat(row.qty as string)),
header: 'Kuantitas',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
formatNumber(parseFloat(row.avg_weight as string)),
header: 'Avg. Bobot (Kg)',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
formatCurrency(parseFloat(row.total_price as string)),
header: 'Total Penjualan (Rp)',
},
{
header: 'Aksi',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) => (
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
<>
<Button
color='warning'
className='px-2 py-1 text-sm'
onClick={() =>
onEditRef.current(props.row.original.id as number)
}
type='button'
>
<Icon icon='mdi:edit' width={16} height={16} /> Edit
</Button>
{/* <Button
color='error'
className='p-1'
onClick={() =>
onDeleteRef.current(props.row.original.id as number)
}
type='button'
>
<Icon icon='mdi:trash' width={16} height={16} />
</Button> */}
</>
</div>
),
},
];
if (formType == 'add_deliver') {
return cols.filter(
(col) => col.header != 'Aksi' && col.header != 'No. Pengiriman'
);
}
return cols;
}, [formType, onInputDate, onEditRef]);
return (
<>
<Table<DeliveryOrderProductFormValues>
rowSelection={rowSelection}
setRowSelection={setRowSelection}
data={data}
columns={columns}
className={{
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-2 py-2 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-2 py-2 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
emptyContent={
<div
className={cn(
'w-full h-16 flex flex-col justify-center items-center gap-2'
)}
>
<span className='text-gray-500'>Belum ada data pengiriman</span>
</div>
}
/>
<div className='flex flex-row gap-3 mt-3'>
{/* <Button
type='button'
variant='outline'
className='justify-start w-fit py-1 text-sm'
onClick={onAddProductClick}
// disabled={!canAddData}
>
<Icon icon='mdi:plus' width={16} height={16} />
Tambah Pengiriman
</Button> */}
{selectedRowIds.length > 0 && (
<Button
type='button'
variant='outline'
color='error'
className='justify-start w-fit py-1 text-sm'
onClick={onBulkDelete}
>
<Icon icon='mdi:trash' width={16} height={16} />
Hapus
{selectedRowIds.length > 0
? ` (${selectedRowIds.length})`
: ''}{' '}
Pengiriman
</Button>
)}
</div>
</>
);
};
export default DeliveryOrderProductTable;
@@ -0,0 +1,203 @@
'use client';
import Button from '@/components/Button';
import Table from '@/components/Table';
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import {
cn,
formatCurrency,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import { Icon } from '@iconify/react';
import { useMemo, useRef, useState } from 'react';
import * as TanStack from '@tanstack/react-table';
import CheckboxInput from '@/components/input/CheckboxInput';
type SalesOrderProductTableProps = {
data: SalesOrderProductFormValues[];
formType?: 'add' | 'edit' | 'deliver';
rowSelection: Record<string, boolean>;
setRowSelection: React.Dispatch<
React.SetStateAction<Record<string, boolean>>
>;
selectedRowIds: number[];
onDelete: (id: number) => void;
onBulkDelete: () => void;
onAddProductClick: () => void;
};
const SalesOrderProductTable = ({
data,
formType,
rowSelection,
setRowSelection,
selectedRowIds,
onDelete,
onBulkDelete,
onAddProductClick,
}: SalesOrderProductTableProps) => {
const onDeleteRef = useRef(onDelete);
onDeleteRef.current = onDelete;
const columns = useMemo(
() => [
{
id: 'select',
header: ({
table,
}: {
table: TanStack.Table<SalesOrderProductFormValues>;
}) => (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
</div>
),
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) => (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
value={`${row.original.product_warehouse_id}${row.original.kandang_id}`}
/>
</div>
),
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatVechicleNumber(row.vehicle_number as string),
header: 'No. Polisi',
},
{
accessorFn: (row: SalesOrderProductFormValues) => row.kandang?.label,
header: 'Kandang',
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
row.product_warehouse?.label,
header: 'Produk',
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatCurrency(parseFloat(row.unit_price as string)),
header: 'Harga Satuan (Rp)',
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatNumber(parseFloat(row.total_weight as string)),
header: 'Total Bobot (Kg)',
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatNumber(parseFloat(row.qty as string)),
header: 'Kuantitas',
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatNumber(parseFloat(row.avg_weight as string)),
header: 'Avg. Bobot (Kg)',
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatCurrency(parseFloat(row.total_price as string)),
header: 'Total Penjualan (Rp)',
},
{
header: 'Aksi',
cell: (
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
) => (
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
<Button
color='error'
className='p-1'
onClick={() =>
onDeleteRef.current(props.row.original.id as number)
}
type='button'
>
<Icon icon='mdi:trash' width={16} height={16} />
</Button>
</div>
),
},
],
[]
);
return (
<>
<Table<SalesOrderProductFormValues>
rowSelection={rowSelection}
setRowSelection={setRowSelection}
data={data}
columns={
formType == 'deliver'
? columns.filter(
(col) => col.header != 'Aksi' && col.id != 'select'
)
: columns
}
className={{
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-2 py-2 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-2 py-2 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
paginationClassName: 'hidden',
}}
emptyContent={
<div
className={cn(
'w-full h-16 flex flex-col justify-center items-center gap-2'
)}
>
<span className='text-gray-500'>Belum ada data penjualan</span>
</div>
}
/>
{formType != 'deliver' && (
<div className='flex flex-row gap-3 mt-3'>
<Button
type='button'
variant='outline'
className='justify-start w-fit py-1 text-sm'
onClick={onAddProductClick}
>
<Icon icon='mdi:plus' width={16} height={16} />
Tambah Produk
</Button>
{selectedRowIds.length > 0 && (
<Button
type='button'
variant='outline'
color='error'
className='justify-start w-fit py-1 text-sm'
onClick={onBulkDelete}
>
<Icon icon='mdi:trash' width={16} height={16} />
Hapus
{selectedRowIds.length > 0
? ` (${selectedRowIds.length})`
: ''}{' '}
Produk
</Button>
)}
</div>
)}
</>
);
};
export default SalesOrderProductTable;
@@ -0,0 +1,235 @@
import Button from '@/components/Button';
import { BaseDeliveryOrder, Marketing } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
import { useMemo, useState } from 'react';
import pdfStyles from './styles/MarketingPDFStyles';
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
import { format } from 'path';
import { date } from 'yup';
interface DeliveryOrderExportProps {
data?: Marketing;
deliveryOrder: BaseDeliveryOrder;
className?: string;
}
const DeliveryOrderExport = ({
data,
deliveryOrder,
}: DeliveryOrderExportProps) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const salesData = data;
const handleDownloadPDF = async () => {
if (!salesData) {
alert('No sales order data available');
return;
}
setIsGeneratingPDF(true);
try {
const blob = await pdf(
<PDFDocument data={salesData} deliveryOrder={deliveryOrder} />
).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${deliveryOrder?.do_number || 'delivery-order'}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error generating PDF:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
setIsGeneratingPDF(false);
}
};
if (!salesData) {
return (
<div className='flex items-center justify-center min-h-screen'>
<div className='text-gray-500'>No sales order data available</div>
</div>
);
}
return salesData?.so_number && salesData.so_number !== 'Belum dibuat' ? (
<Button
color='primary'
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
onClick={handleDownloadPDF}
isLoading={isGeneratingPDF}
>
<Icon icon='material-symbols:file-open-outline' width={16} height={16} />
{deliveryOrder.do_number}
</Button>
) : null;
};
export default DeliveryOrderExport;
const PDFDocument = ({
data,
deliveryOrder,
}: {
data: Marketing;
deliveryOrder: BaseDeliveryOrder;
}) => {
const grandTotal = useMemo(() => {
return (
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
);
}, []);
return (
<Document>
<Page size='A4' style={pdfStyles.page}>
{/* Header Section */}
<View style={pdfStyles.header}>
<Image
src={'https://placehold.co/120x30/png'}
style={pdfStyles.logo}
id={'mbu-logo'}
/>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
{/* Delivery Order Title */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.title}>DELIVERY ORDER</Text>
<View style={pdfStyles.poInfo}>
<Text>{deliveryOrder.do_number || '-'}</Text>
</View>
</View>
{/* Depature Table */}
<View style={pdfStyles.table}>
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellHeader}>
<Text>Ship To</Text>
</View>
<View style={pdfStyles.tableCellHeaderLast}>
<Text>Depature From</Text>
</View>
</View>
<View style={pdfStyles.tableRow}>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
{data?.customer?.name || '-'} ({data?.customer?.type || '-'})
</Text>
<Text style={{ marginTop: '2px' }}>
{data?.customer.email || ''} - {data?.customer.phone || ''}
</Text>
<Text></Text>
<Text>{data?.customer.address || ''}</Text>
<Text style={{ fontSize: '7px', marginTop: '3px' }}></Text>
</View>
<View style={pdfStyles.tableCellLast}>
<Text style={{ fontWeight: 'bold' }}>
{deliveryOrder.warehouse?.name || '-'}
</Text>
<Text style={{ marginTop: '2px' }}>
{formatDate(deliveryOrder.delivery_date, 'DD MMM YYYY')}
</Text>
<Text>{deliveryOrder.warehouse?.area?.name}</Text>
</View>
</View>
</View>
{/* Delivery Table */}
<Text style={pdfStyles.sectionTitle}>Product Shipped</Text>
<View style={pdfStyles.table}>
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellHeader}>
<Text>Item Description</Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text>Vehicle Number</Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text>Unit Price</Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text>Quantity</Text>
</View>
<View style={pdfStyles.tableCellHeaderLast}>
<Text>Total Amount</Text>
</View>
</View>
{deliveryOrder.deliveries?.map((item, index) => {
return (
<View key={index} style={[pdfStyles.tableRow]}>
<View style={pdfStyles.tableCell}>
<Text>{item.product_warehouse?.product?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCell}>
<Text>
{formatVechicleNumber(item.vehicle_number) || '-'}
</Text>
</View>
<View style={pdfStyles.tableCellRight}>
<Text>Rp{formatNumber(item.unit_price || 0)}</Text>
</View>
<View style={pdfStyles.tableCellRight}>
<Text>{formatNumber(item.qty || 0)}</Text>
</View>
<View style={pdfStyles.tableCellRightLast}>
<Text>Rp{formatNumber(item.total_price || 0)}</Text>
</View>
</View>
);
}) || []}
{/* Grand Total Row inside table */}
<View style={pdfStyles.grandTotalRow}>
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { borderRightWidth: 0 }]}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellRight}>
<Text style={{ fontWeight: 'bold' }}>Grand Total</Text>
</View>
<View
style={[pdfStyles.tableCellRightLast, { fontWeight: 'bold' }]}
>
<Text>Rp{formatNumber(grandTotal)}</Text>
</View>
</View>
</View>
{/* Footer with Special Instructions */}
<View style={pdfStyles.footer}>
<View style={pdfStyles.specialInstructionTable}>
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellHeaderLast}>
<Text>From Sales Order</Text>
</View>
</View>
<View style={pdfStyles.tableRow}>
<View style={pdfStyles.tableCellLast}>
<Text>
{data?.so_number || '-'} -{' '}
{formatDate(data.so_date, 'DD MMM YYYY')}
</Text>
</View>
</View>
</View>
<View style={pdfStyles.footerCompany}>
<Text>PT LUMBUNG TELUR INDONESIA</Text>
</View>
</View>
</Page>
</Document>
);
};
@@ -0,0 +1,227 @@
import Button from '@/components/Button';
import { Marketing } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
import { useMemo, useState } from 'react';
import pdfStyles from './styles/MarketingPDFStyles';
import { formatDate, formatNumber } from '@/lib/helper';
interface SalesOrderExportProps {
data?: Marketing;
className?: string;
}
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const salesData = data;
const handleDownloadPDF = async () => {
if (!salesData) {
alert('No sales order data available');
return;
}
setIsGeneratingPDF(true);
try {
const blob = await pdf(<PDFDocument data={salesData} />).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${salesData?.so_number || 'sales-order'}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error generating PDF:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
setIsGeneratingPDF(false);
}
};
if (!salesData) {
return (
<div className='flex items-center justify-center min-h-screen'>
<div className='text-gray-500'>No sales order data available</div>
</div>
);
}
return salesData?.so_number && salesData.so_number !== 'Belum dibuat' ? (
<Button
color='primary'
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
onClick={handleDownloadPDF}
isLoading={isGeneratingPDF}
>
<Icon icon='material-symbols:file-open-outline' width={16} height={16} />
{salesData.so_number}
</Button>
) : null;
};
export default SalesOrderExport;
const PDFDocument = ({ data }: { data: Marketing }) => {
const grandTotal = useMemo(() => {
return data?.sales_order?.reduce((a, b) => a + b.total_price, 0) ?? 0;
}, [data?.sales_order]);
return (
<Document>
<Page size='A4' style={pdfStyles.page}>
{/* Header Section */}
<View style={pdfStyles.header}>
<Image
src={'https://placehold.co/120x30/png'}
style={pdfStyles.logo}
id={'mbu-logo'}
/>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
{/* Sales Order Title */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.title}>SALES ORDER</Text>
<View style={pdfStyles.poInfo}>
<Text>SO Number: {data?.so_number || '-'}</Text>
<Text>
Date:{' '}
{data?.so_date
? formatDate(data.so_date, 'DD MMM YYYY')
: formatDate(new Date(), 'DD MMM YYYY')}
</Text>
</View>
</View>
{/* Customer Table */}
<View style={pdfStyles.table}>
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellHeader}>
<Text>Customer</Text>
</View>
<View style={pdfStyles.tableCellHeaderLast}>
<Text>Sales</Text>
</View>
</View>
<View style={pdfStyles.tableRow}>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
{data?.customer?.name || '-'} ({data?.customer?.type || '-'})
</Text>
<Text style={{ marginTop: '2px' }}>
{data?.customer.email || ''} - {data?.customer.phone || ''}
</Text>
<Text></Text>
<Text>{data?.customer.address || ''}</Text>
<Text style={{ fontSize: '7px', marginTop: '3px' }}></Text>
</View>
<View style={pdfStyles.tableCellLast}>
<Text style={{ fontWeight: 'bold' }}>
PT LUMBUNG TELUR INDONESIA
</Text>
<Text style={{ fontWeight: 'bold', marginTop: '2px' }}>
{data?.sales_person?.name || '-'}
</Text>
<Text>{data?.sales_person.email}</Text>
</View>
</View>
</View>
{/* Product Sales Order Table */}
<Text style={pdfStyles.sectionTitle}>Product Sold</Text>
<View style={pdfStyles.table}>
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellHeader}>
<Text>Item Description</Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text>From</Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text>Unit Price</Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text>Quantity</Text>
</View>
<View style={pdfStyles.tableCellHeaderLast}>
<Text>Total Amount</Text>
</View>
</View>
{data?.sales_order?.map((item, index) => {
const isLastItem = index === (data?.sales_order?.length || 0) - 1;
return (
<View
key={index}
style={[
pdfStyles.tableRow,
// isLastItem ? {} : pdfStyles.tableBorderBottom,
]}
>
<View style={pdfStyles.tableCell}>
<Text>{item.product_warehouse?.product?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCell}>
<Text>{item.product_warehouse?.warehouse?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCellRight}>
<Text>Rp{formatNumber(item.unit_price || 0)}</Text>
</View>
<View style={pdfStyles.tableCellRight}>
<Text>{formatNumber(item.qty || 0)}</Text>
</View>
<View style={pdfStyles.tableCellRightLast}>
<Text>Rp{formatNumber(item.total_price || 0)}</Text>
</View>
</View>
);
}) || []}
{/* Grand Total Row inside table */}
<View style={pdfStyles.grandTotalRow}>
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { borderRightWidth: 0 }]}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellRight}>
<Text style={{ fontWeight: 'bold' }}>Grand Total</Text>
</View>
<View
style={[pdfStyles.tableCellRightLast, { fontWeight: 'bold' }]}
>
<Text>Rp{formatNumber(grandTotal)}</Text>
</View>
</View>
</View>
{/* Footer with Special Instructions */}
<View style={pdfStyles.footer}>
<View style={pdfStyles.specialInstructionTable}>
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellHeaderLast}>
<Text>Notes</Text>
</View>
</View>
<View style={pdfStyles.tableRow}>
<View style={pdfStyles.tableCellLast}>
<Text>{data?.notes || '-'}</Text>
</View>
</View>
</View>
<View style={pdfStyles.footerCompany}>
<Text>PT LUMBUNG TELUR INDONESIA</Text>
</View>
</View>
</Page>
</Document>
);
};
@@ -0,0 +1,212 @@
import { StyleSheet } from '@react-pdf/renderer';
const pdfStyles = StyleSheet.create({
page: {
fontSize: 10,
fontFamily: 'Helvetica',
padding: 20,
backgroundColor: '#FFFFFF',
},
header: {
marginBottom: 20,
},
logo: {
width: 120,
height: 30,
marginBottom: 8,
},
companyInfo: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 4,
color: '#1f74bf',
},
address: {
fontSize: 8,
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: 9,
textAlign: 'right',
},
sectionTitle: {
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: 8,
fontSize: 9,
},
tableCellLast: {
flex: 1,
padding: 8,
fontSize: 9,
},
tableCellHeader: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 8,
fontSize: 9,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
},
tableCellHeaderLast: {
flex: 1,
padding: 8,
fontSize: 9,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
},
tableCellRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 8,
fontSize: 9,
textAlign: 'right',
},
tableCellRightLast: {
flex: 1,
padding: 8,
fontSize: 9,
textAlign: 'right',
},
tableBorderBottom: {
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
},
grandTotalRow: {
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: '#000000',
borderTopStyle: 'solid',
},
grandTotalLabel: {
flex: 3,
padding: 8,
fontSize: 9,
fontWeight: 'bold',
textAlign: 'right',
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
},
grandTotalValue: {
flex: 1,
padding: 8,
fontSize: 9,
fontWeight: 'bold',
textAlign: 'right',
borderRightWidth: 0,
},
allocationSection: {
marginBottom: 15,
},
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: 8,
fontSize: 9,
},
innerCellLast: {
flex: 1,
padding: 8,
fontSize: 9,
},
innerCellRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 8,
fontSize: 9,
textAlign: 'right',
},
innerCellRightLast: {
flex: 1,
padding: 8,
fontSize: 9,
textAlign: 'right',
},
footer: {
marginTop: 30,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
footerCompany: {
fontSize: 12,
fontWeight: 'bold',
textAlign: 'right',
flex: 1,
color: '#1f74bf',
},
specialInstructionTable: {
width: '60%',
maxWidth: 300,
borderWidth: 1,
borderColor: '#000000',
flex: 1,
},
});
export default pdfStyles;
@@ -1,308 +0,0 @@
'use client';
import Button from '@/components/Button';
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import {
cn,
formatCurrency,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { useState } from 'react';
import toast from 'react-hot-toast';
const SalesOrderDetail = ({
initialValues,
refreshValues,
}: {
initialValues?: Marketing;
refreshValues?: () => void;
}) => {
const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>(
'approve'
);
const [isLoading, setIsLoading] = useState(false);
const deleteModal = useModal();
const confirmationModal = useModal();
const deliveryModal = useModal();
const approveClickHandler = () => {
setApprovalAction('approve');
confirmationModal.openModal();
};
const rejectClickHandler = () => {
setApprovalAction('reject');
confirmationModal.openModal();
};
const deliveryClickHandler = () => {
deliveryModal.openModal();
};
const deleteClickHandler = () => {
deleteModal.openModal();
};
const confirmationModalDeleteClickHandler = async () => {
setIsLoading(true);
// await MarketingApi.delete(initialValues?.id as number);
setIsLoading(false);
deleteModal.closeModal();
toast.success('Successfully deleted Sales Order!');
refreshValues?.();
};
const confirmationModalApproveClickHandler = async () => {
setIsLoading(true);
// await MarketingApi.singleApproval(
// initialValues?.id as number,
// approvalAction
// );
setIsLoading(false);
confirmationModal.closeModal();
toast.success('Successfully approved Sales Order!');
refreshValues?.();
};
const confirmationModalDeliveryClickHandler = async () => {
setIsLoading(true);
// await MarketingApi.delivery(initialValues?.id as number);
setIsLoading(false);
deliveryModal.closeModal();
toast.success('Successfully delivered Sales Order!');
refreshValues?.();
};
return (
<>
<div className='flex flex-col w-full gap-4'>
<FormHeader
title='Detail Sales Order'
backUrl='/marketing/sales-orders'
/>
<div className='flex-row flex gap-3'>
{initialValues?.approval?.step_number != 3 && (
<>
<Button
color='success'
onClick={approveClickHandler}
disabled={initialValues?.approval?.step_number != 1}
>
<Icon icon='mdi:check' width={24} height={24} />
Approve
</Button>
<Button
color='error'
onClick={rejectClickHandler}
disabled={initialValues?.approval?.step_number != 2}
>
<Icon icon='mdi:close' width={24} height={24} />
Reject
</Button>
</>
)}
{initialValues?.approval?.step_number == 2 && (
<Button color='success' onClick={deliveryClickHandler}>
<Icon icon='mdi:check' width={24} height={24} />
Delivery Order
</Button>
)}
</div>
<Card
title='Informasi Sales Order'
className={{
wrapper: 'w-full bg-white',
}}
>
<div className='overflow-x-auto rounded-box border border-base-content/5 bg-base-100 mt-3'>
<table className='table'>
<tbody>
<tr>
<td width='45%' className='font-semibold'>
No. Sales Order
</td>
<td>:</td>
<td width='50%'>{initialValues?.so_number}</td>
</tr>
<tr>
<td className='font-semibold'>Nama Pelanggan</td>
<td>:</td>
<td>{initialValues?.customer?.name}</td>
</tr>
<tr>
<td className='font-semibold'>Status</td>
<td>:</td>
<td>{initialValues?.approval?.step_name}</td>
</tr>
<tr>
<td className='font-semibold'>Tanggal Penjualan</td>
<td>:</td>
<td>{initialValues?.so_date}</td>
</tr>
<tr>
<td className='font-semibold'>Total Penjualan</td>
<td>:</td>
<td>
{formatCurrency(initialValues?.grand_total as number)}
</td>
</tr>
<tr>
<td className='font-semibold'>Catatan</td>
<td>:</td>
<td>{initialValues?.notes ?? '-'}</td>
</tr>
</tbody>
</table>
</div>
</Card>
{initialValues?.marketing_products && (
<Card
title='Daftar Produk'
className={{
wrapper: 'w-full bg-white',
}}
>
<Table<MarketingProduct>
data={initialValues?.marketing_products}
columns={[
{
header: 'No. Polisi',
accessorFn(row) {
return formatVechicleNumber(
row.marketing_delivery_products?.vehicle_number as string
);
},
},
{
header: 'Kandang',
accessorFn(row) {
return row.product_warehouse.warehouse.name;
},
},
{
header: 'Produk',
accessorFn(row) {
return row.product_warehouse.product.name;
},
},
{
header: 'Harga Satuan (Rp)',
accessorFn(row) {
return formatCurrency(row.unit_price);
},
},
{
header: 'Total Bobot (Kg)',
accessorFn(row) {
return formatNumber(row.total_weight);
},
},
{
header: 'Kuantitas',
accessorFn(row) {
return formatNumber(row.qty);
},
},
{
header: 'Avg. Bobot (Kg)',
accessorFn(row) {
return formatNumber(row.avg_weight);
},
},
{
header: 'Total Penjualan (Rp)',
accessorFn(row) {
return formatCurrency(row.total_price);
},
},
]}
className={{
containerClassName: cn({
'mb-20':
initialValues?.marketing_products &&
initialValues?.marketing_products?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
/>
</Card>
)}
<div className='flex flex-row gap-3'>
<Button
color='warning'
type='button'
href={`/marketing/sales-orders/detail/edit?salesOrderId=${initialValues?.id}`}
>
<Icon icon='mdi:pencil' width={24} height={24} />
Edit
</Button>
<Button color='error' onClick={deleteClickHandler}>
<Icon icon='mdi:delete' width={24} height={24} />
Hapus
</Button>
</div>
</div>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
<ConfirmationModal
ref={confirmationModal.ref}
type={approvalAction === 'approve' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: approvalAction === 'approve' ? 'success' : 'error',
isLoading: isLoading,
onClick: confirmationModalApproveClickHandler,
}}
/>
<ConfirmationModal
ref={deliveryModal.ref}
type={'success'}
text={`Apakah anda yakin ingin deliver penjualan ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'success',
isLoading: isLoading,
onClick: confirmationModalDeliveryClickHandler,
}}
/>
</>
);
};
export default SalesOrderDetail;
@@ -1,38 +0,0 @@
import * as Yup from 'yup';
import { MarketingProduct } from '@/types/api/marketing/marketing';
import {
MarketingProductFormValues,
MarketingProductSchema,
} from './repeater/MarketingProduct.schema';
type MarketingSchema = {
customer_id: number | undefined;
customer:
| {
value: number;
label: string;
}
| undefined
| null;
so_date: string | undefined;
notes: string | undefined;
marketing_products: MarketingProductFormValues[];
};
export const MarketingSchema: Yup.ObjectSchema<MarketingSchema> = Yup.object({
customer_id: Yup.number().required('Customer wajib diisi!'),
customer: Yup.object({
value: Yup.number().required(),
label: Yup.string().required(),
}).nullable(),
so_date: Yup.string().required('Tanggal wajib diisi!'),
notes: Yup.string().required('Catatan wajib diisi!'),
marketing_products: Yup.array()
.of(MarketingProductSchema)
.min(1, 'Minimal harus ada 1 produk!')
.required('Produk wajib diisi!'),
});
export const UpdateMarketingSchema = MarketingSchema;
export type MarketingFormValues = Yup.InferType<typeof MarketingSchema>;
@@ -1,514 +0,0 @@
'use client';
import Button from '@/components/Button';
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import DateInput from '@/components/input/DateInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import TextArea from '@/components/input/TextArea';
import Modal, { useModal } from '@/components/Modal';
import * as TanStack from '@tanstack/react-table';
import Table from '@/components/Table'; // Keep this import
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
import {
CreateMarketingPayload,
CreateMarketingProductPayload,
Marketing,
MarketingProduct,
} from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import MarketingProductForm from './repeater/MarketingProductForm';
import CheckboxInput from '@/components/input/CheckboxInput';
import { Customer } from '@/types/api/master-data/customer';
import { CustomerApi } from '@/services/api/master-data';
import { useFormik } from 'formik';
import { MarketingFormValues, MarketingSchema } from './SalesForm.schema';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { MarketingProductFormValues } from './repeater/MarketingProduct.schema';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
const SalesForm = ({
formType = 'add',
initialValues,
}: {
formType?: 'add' | 'edit';
initialValues?: Marketing;
}) => {
const router = useRouter();
const addProductModal = useModal();
const deleteModal = useModal();
const [isLoading, setIsLoading] = useState(false);
const [selectedMarketingProduct, setSelectedMarketingProduct] =
useState<MarketingProduct | null>(null);
const [rawMarketingProducts, setRawMarketingProducts] = useState<
MarketingProduct[]
>(initialValues?.marketing_products || []);
const [selectedCustomer, setSelectedCustomer] = useState<OptionType | null>(
initialValues?.customer
? { value: initialValues.customer.id, label: initialValues.customer.name }
: null
);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item)
);
const [grandTotal, setGrandTotal] = useState<number>(
initialValues?.grand_total ?? 0
);
const marketingProducts = useMemo(
() => rawMarketingProducts,
[rawMarketingProducts]
);
const {
options: customerOptions,
rawData: customerRawData,
isLoadingOptions: isLoadingCustomerOptions,
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
const handleAddProduct = useCallback(() => {
addProductModal.openModal();
}, [addProductModal]);
const handleDeleteProduct = useCallback((id: number) => {
setRawMarketingProducts((prev) => prev.filter((p) => p.id !== id));
}, []);
const handleBulkDeleteProduct = () => {
setRawMarketingProducts((prev) =>
prev.filter((product) => !selectedRowIds.includes(product.id))
);
};
const handleDelete = () => {
deleteModal.openModal();
};
const handleAddSubmitProduct = useCallback(
async (
tableValue: CreateMarketingProductPayload,
fieldValues: MarketingProductFormValues
) => {
const newMarketingProduct: MarketingProduct = {
id: rawMarketingProducts.length + 1,
product_warehouse: tableValue.product_warehouse!,
unit_price: tableValue.unit_price as number,
total_weight: tableValue.total_weight as number,
qty: tableValue.qty as number,
avg_weight: tableValue.avg_weight as number,
total_price: tableValue.total_price as number,
marketing_delivery_products: {
id: rawMarketingProducts.length + 1,
vehicle_number: tableValue.vehicle_number as string,
delivery_date: tableValue.delivery_date as string,
unit_price: tableValue.unit_price as number,
total_weight: tableValue.total_weight as number,
qty: tableValue.qty as number,
avg_weight: tableValue.avg_weight as number,
total_price: tableValue.total_price as number,
},
};
setRawMarketingProducts((prev) => [...prev, newMarketingProduct]);
formik.setValues({
...formik.values,
marketing_products: [...formik.values.marketing_products, fieldValues],
});
setGrandTotal((prev) => prev + (tableValue.total_price as number));
addProductModal.closeModal();
},
[rawMarketingProducts.length, addProductModal]
);
const handleChangeCustomer = useCallback(
(val: OptionType | OptionType[] | null) => {
setSelectedCustomer(val as OptionType);
formik.setFieldValue('customer_id', (val as OptionType)?.value);
formik.setFieldValue('customer', val as OptionType);
},
[selectedCustomer, setSelectedCustomer]
);
const createMarketingHandler = async (values: CreateMarketingPayload) => {
console.log(values);
const createMarketingRes = await MarketingApi.create(values);
if (isResponseSuccess(createMarketingRes)) {
console.log(createMarketingRes);
}
if (isResponseError(createMarketingRes)) {
console.log(createMarketingRes);
}
toast.success('Successfully created Sales Order!');
router.push('/marketing/sales-orders');
};
const updateMarketingHandler = async (values: CreateMarketingPayload) => {
console.log(values);
const createMarketingRes = await MarketingApi.update(
initialValues?.id as number,
values
);
if (isResponseSuccess(createMarketingRes)) {
console.log(createMarketingRes);
}
if (isResponseError(createMarketingRes)) {
console.log(createMarketingRes);
}
toast.success('Successfully updated Sales Order!');
router.push('/marketing/sales-orders');
};
const deleteMarketingHandler = async () => {
setIsLoading(true);
console.log(initialValues?.id);
const deleteMarketingRes = await MarketingApi.delete(
initialValues?.id as number
);
if (isResponseSuccess(deleteMarketingRes)) {
console.log(deleteMarketingRes);
}
if (isResponseError(deleteMarketingRes)) {
console.log(deleteMarketingRes);
}
toast.success('Successfully deleted Sales Order!');
setIsLoading(false);
deleteModal.closeModal();
router.push('/marketing/sales-orders');
};
const MarketingProductToFieldValues = (
product: MarketingProduct
): MarketingProductFormValues => {
return {
vehicle_number: product.marketing_delivery_products?.vehicle_number,
kandang_id: product.product_warehouse.warehouse.id,
kandang: {
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
},
product_warehouse: {
value: product.product_warehouse.product.id,
label: product.product_warehouse.product.name,
},
product_warehouse_id: product.product_warehouse.product.id,
unit_price: product.unit_price,
total_weight: product.total_weight,
qty: product.qty,
uom: product.product_warehouse?.product?.uom?.name,
avg_weight: product.avg_weight,
total_price: product.total_price,
delivery_date: product.marketing_delivery_products?.delivery_date,
};
};
const formik = useFormik<MarketingFormValues>({
enableReinitialize: true,
initialValues: {
so_date: initialValues?.so_date || undefined,
notes: initialValues?.notes || undefined,
customer_id: initialValues?.customer?.id || undefined,
customer: {
value: initialValues?.customer?.id as number,
label: initialValues?.customer?.name as string,
},
marketing_products:
initialValues?.marketing_products?.map((product) =>
MarketingProductToFieldValues(product)
) ?? [],
},
validationSchema: MarketingSchema,
onSubmit: async (values) => {
const payload = {
customer_id: values.customer_id as number,
date: values.so_date as string,
notes: values.notes as string,
marketing_products: values.marketing_products,
} as CreateMarketingPayload;
switch (formType) {
case 'add':
createMarketingHandler(payload);
break;
case 'edit':
updateMarketingHandler(payload);
break;
default:
break;
}
},
});
const { setValues: formikSetValues } = formik;
useEffect(() => {
formikSetValues(formik.initialValues);
}, [formikSetValues, formik.initialValues]);
const columns = useMemo(
() => [
{
id: 'select',
header: ({ table }: { table: TanStack.Table<MarketingProduct> }) => (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
</div>
),
cell: ({ row }: { row: TanStack.Row<MarketingProduct> }) => (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
),
},
{
accessorFn: (row: MarketingProduct) =>
row.marketing_delivery_products?.vehicle_number,
header: 'No. Polisi',
},
{
accessorFn: (row: MarketingProduct) =>
row.product_warehouse.warehouse.name,
header: 'Kandang',
},
{
accessorFn: (row: MarketingProduct) =>
row.product_warehouse.product.name,
header: 'Produk',
},
{
accessorFn: (row: MarketingProduct) => formatCurrency(row.unit_price),
header: 'Harga Satuan (Rp)',
},
{
accessorFn: (row: MarketingProduct) => formatNumber(row.total_weight),
header: 'Total Bobot (Kg)',
},
{
accessorFn: (row: MarketingProduct) => formatNumber(row.qty),
header: 'Kuantitas',
},
{
accessorFn: (row: MarketingProduct) => formatNumber(row.avg_weight),
header: 'Avg. Bobot (Kg)',
},
{
accessorFn: (row: MarketingProduct) => formatCurrency(row.total_price),
header: 'Total Penjualan (Rp)',
},
{
header: 'Aksi',
cell: (props: TanStack.CellContext<MarketingProduct, unknown>) => (
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
<Button
color='error'
className='p-1'
onClick={() => handleDeleteProduct(props.row.original.id)}
>
<Icon icon='mdi:trash' width={16} height={16} />
</Button>
</div>
),
},
],
[handleDeleteProduct] // dependensi tunggal
);
return (
<>
<form
className='flex flex-col gap-4'
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
>
<FormHeader
title={`${formType === 'add' ? 'Tambah' : 'Edit'} Sales Order`}
backUrl='/marketing/sales-orders'
/>
<Card
title='Informasi Order'
className={{
wrapper: 'bg-white w-full',
}}
>
<div className='grid grid-cols-2 gap-3 mt-3'>
<SelectInput
label='Pelanggan'
options={customerOptions}
isLoading={isLoadingCustomerOptions}
value={selectedCustomer}
onChange={handleChangeCustomer}
isError={
formik.touched.customer_id && Boolean(formik.errors.customer_id)
}
errorMessage={formik.errors.customer_id}
isClearable
placeholder='Pilih Pelanggan'
/>
<DateInput
name='so_date'
label='Tanggal'
value={formik.values.so_date}
onChange={formik.handleChange}
isError={formik.touched.so_date && Boolean(formik.errors.so_date)}
errorMessage={formik.errors.so_date}
placeholder='Pilih Tanggal'
/>
</div>
</Card>
<Card
title='Daftar Produk'
className={{
wrapper: 'bg-white w-full',
}}
>
<Table<MarketingProduct>
rowSelection={rowSelection}
setRowSelection={setRowSelection}
data={marketingProducts}
columns={columns}
className={{
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-2 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-2 py-2 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
paginationClassName: 'hidden',
}}
emptyContent={
<div
className={cn(
'w-full h-16 flex flex-col justify-center items-center gap-2'
)}
>
<span className='text-gray-500'>Belum ada data penjualan</span>
</div>
}
/>
<div className='flex flex-row gap-3 mt-3'>
<Button
type='button'
variant='outline'
className='justify-start w-fit py-1 text-sm'
onClick={handleAddProduct}
>
<Icon icon='mdi:plus' width={16} height={16} />
Tambah Produk
</Button>
{selectedRowIds.length > 0 && (
<Button
type='button'
variant='outline'
color='error'
className='justify-start w-fit py-1 text-sm'
onClick={handleBulkDeleteProduct}
>
<Icon icon='mdi:trash' width={16} height={16} />
Hapus
{selectedRowIds.length > 0
? ` (${selectedRowIds.length})`
: ''}{' '}
Produk
</Button>
)}
</div>
</Card>
<div className='grid grid-cols-2 gap-3'>
<TextArea
required
name='notes'
label='Catatan'
rows={3}
placeholder='Masukan catatan penjualan'
value={formik.values.notes}
onChange={formik.handleChange}
isError={formik.touched.notes && Boolean(formik.errors.notes)}
errorMessage={formik.errors.notes}
/>
<div className='flex flex-col h-full justify-between items-end py-6'>
<span>Total Penjualan</span>
<span className='text-lg font-semibold'>
{formatCurrency(grandTotal)}
</span>
</div>
</div>
<div className='flex flex-row items-start justify-center gap-2 mt-4'>
<Button type='reset' color='warning' disabled={formik.isSubmitting}>
Reset
</Button>
<Button
type='submit'
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</div>
</form>
{formType == 'edit' && (
<div className='flex flex-row justify-start'>
<Button type='button' color='error' onClick={handleDelete}>
<Icon icon='mdi:trash' width={24} height={24} />
Hapus
</Button>
</div>
)}
<Modal
ref={addProductModal.ref}
closeOnBackdrop
className={{
modalBox: 'max-w-4/5 z-100',
}}
>
<div className='flex flex-col gap-4'>
<div className='flex flex-row items-center justify-between'>
<h3 className='text-lg font-semibold mb-4'>Tambah Produk</h3>
<Button
variant='ghost'
color='error'
className='rounded-full'
onClick={addProductModal.closeModal}
>
<Icon icon='mdi:close' width={20} height={20} />
</Button>
</div>
<div>
<MarketingProductForm
onSubmitForm={handleAddSubmitProduct}
modalRef={addProductModal.ref}
data={rawMarketingProducts}
initialValues={selectedMarketingProduct ?? undefined}
/>
</div>
</div>
</Modal>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
onClick: deleteMarketingHandler,
}}
/>
</>
);
};
export default SalesForm;
@@ -1,26 +1,48 @@
import * as Yup from 'yup';
export const KandangFormSchema = Yup.object({
name: Yup.string().required('Nama wajib diisi!'),
type KandangFormSchemaType = {
name: string;
locationId: number | undefined;
location:
| {
value: number;
label: string;
}
| undefined
| null;
capacity: number | undefined;
picId: number | undefined;
pic:
| {
value: number;
label: string;
}
| undefined
| null;
};
locationId: Yup.number()
.min(1, 'Lokasi wajib diisi!')
.required('Lokasi wajib diisi!'),
location: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
Yup.object({
name: Yup.string().required('Nama wajib diisi!'),
capacity: Yup.number()
.min(1, 'Kapasitas wajib diisi!')
.required('Kapasitas wajib diisi!'),
locationId: Yup.number()
.min(1, 'Lokasi wajib diisi!')
.required('Lokasi wajib diisi!'),
location: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
picId: Yup.number().min(1, 'PIC wajib diisi!').required('PIC wajib diisi!'),
pic: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
});
capacity: Yup.number()
.min(1, 'Kapasitas wajib diisi!')
.required('Kapasitas wajib diisi!'),
picId: Yup.number().min(1, 'PIC wajib diisi!').required('PIC wajib diisi!'),
pic: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
});
export const UpdateKandangFormSchema = KandangFormSchema;
@@ -82,7 +82,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
label: initialValues.location.name,
}
: null,
capacity: initialValues?.capacity ?? 0,
capacity: initialValues?.capacity,
picId: initialValues?.pic?.id ?? 0,
pic: initialValues?.pic
? {
@@ -102,9 +102,9 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
const kandangPayload: CreateKandangPayload = {
name: values.name,
location_id: values.locationId,
capacity: values.capacity,
pic_id: values.picId,
location_id: values.locationId!,
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
pic_id: values.picId!,
};
switch (type) {
@@ -256,7 +256,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
required
name='capacity'
label='Kapasitas'
value={formik.values.capacity ?? undefined}
placeholder='Masukan kapasitas kandang'
value={formik.values.capacity}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
@@ -1,11 +1,17 @@
import * as Yup from 'yup';
export const ProductCategoryFormSchema = Yup.object({
code: Yup.string()
.required('Kode wajib diisi!')
.max(3, 'Kode kategori produk melebihi 3 karakter!'),
name: Yup.string().required('Nama wajib diisi!'),
});
type ProductCategoryFormSchemaType = {
code: string;
name: string;
};
export const ProductCategoryFormSchema: Yup.ObjectSchema<ProductCategoryFormSchemaType> =
Yup.object({
code: Yup.string()
.required('Kode wajib diisi!')
.max(3, 'Kode kategori produk melebihi 3 karakter!'),
name: Yup.string().required('Nama wajib diisi!'),
});
export const UpdateProductCategoryFormSchema = ProductCategoryFormSchema;
@@ -71,12 +71,13 @@ const ProductCategoryForm = ({
[router]
);
const formikInitialValues = useMemo<ProductCategoryFormValues>(() => {
return {
const formikInitialValues = useMemo<ProductCategoryFormValues>(
() => ({
code: initialValues?.code ?? '',
name: initialValues?.name ?? '',
};
}, [initialValues]);
}),
[initialValues]
);
const formik = useFormik<ProductCategoryFormValues>({
initialValues: formikInitialValues,
@@ -118,7 +119,7 @@ const ProductCategoryForm = ({
await ProductCategoryApi.delete(initialValues?.id as number);
deleteModal.closeModal();
toast.success('Successfully delete Product Category!');
toast.success('Berhasil menghapus data Kategori Produk!');
setIsDeleteLoading(false);
router.push('/master-data/product-category');
};
@@ -129,7 +130,7 @@ const ProductCategoryForm = ({
return (
<>
<section className='w-full max-w-xl'>
<section className='w-full max-w-2xl'>
<header className='flex flex-col gap-4'>
<Button
href='/master-data/product-category'
@@ -141,9 +142,9 @@ const ProductCategoryForm = ({
</Button>
<h1 className='text-2xl font-bold text-center'>
{type === 'add' && 'Tambah Product Category'}
{type === 'edit' && 'Edit Product Category'}
{type === 'detail' && 'Detail Product Category'}
{type === 'add' && 'Tambah Kategori Produk'}
{type === 'edit' && 'Edit Kategori Produk'}
{type === 'detail' && 'Detail Kategori Produk'}
</h1>
</header>
@@ -157,7 +158,7 @@ const ProductCategoryForm = ({
required
label='Kode'
name='code'
placeholder='Masukkan kode kategori produk'
placeholder='Masukkan kode...'
value={formik.values.code}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
@@ -169,7 +170,7 @@ const ProductCategoryForm = ({
required
label='Nama'
name='name'
placeholder='Masukkan nama kategori produk'
placeholder='Masukkan nama...'
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
@@ -256,7 +257,7 @@ const ProductCategoryForm = ({
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Product Category ini (${initialValues?.name})?`}
text={`Apakah anda yakin ingin menghapus data Kategori Produk ini (${initialValues?.name})?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -1,53 +1,86 @@
import * as Yup from 'yup';
export const ProductFormSchema = Yup.object({
name: Yup.string().required('Nama wajib diisi!'),
brand: Yup.string().required('Merek wajib diisi!'),
sku: Yup.string().required('SKU wajib diisi!'),
uom: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
uom_id: Yup.number()
.required('Satuan wajib diisi!')
.typeError('Satuan wajib diisi!'),
product_category: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
product_category_id: Yup.number()
.required('Kategori produk wajib diisi!')
.typeError('Kategori produk wajib diisi!'),
product_price: Yup.number()
.required('Harga produk wajib diisi!')
.typeError('Harga produk wajib diisi!')
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
selling_price: Yup.number()
.required('Harga jual wajib diisi!')
.typeError('Harga jual wajib diisi!')
.min(0, 'Harga jual tidak boleh kurang dari 0!'),
tax: Yup.number()
.required('Pajak wajib diisi!')
.typeError('Pajak wajib diisi!')
.min(0, 'Pajak tidak boleh kurang dari 0!')
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!')
.typeError('Periode kadaluarsa wajib diisi!')
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
supplier: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
supplier_ids: Yup.array()
.of(Yup.number().typeError('Supplier tidak valid!'))
.min(1, 'Minimal harus ada 1 supplier!')
.required('Supplier wajib diisi!'),
flags: Yup.array()
.of(Yup.string())
.min(1, 'Minimal harus ada 1 flag!')
.required('Flag wajib diisi!'),
});
type ProductFormSchemaType = {
name: string;
brand: string;
sku: string;
uom?: {
value: number;
label: string;
} | null;
uom_id: number;
product_category?: {
value: number;
label: string;
} | null;
product_category_id: number;
product_price: number | string;
selling_price: number | string;
tax: number | string;
expiry_period: number | string;
supplier_ids: number[];
flags: string[];
};
export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
Yup.object({
name: Yup.string().required('Nama wajib diisi!'),
brand: Yup.string().required('Merek wajib diisi!'),
sku: Yup.string().required('SKU wajib diisi!'),
uom: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
})
.nullable()
.required('Satuan wajib diisi!'),
uom_id: Yup.number()
.required('Satuan wajib diisi!')
.typeError('Satuan wajib diisi!'),
product_category: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
})
.nullable()
.required('Kategori produk wajib diisi!'),
product_category_id: Yup.number()
.required('Kategori produk wajib diisi!')
.typeError('Kategori produk wajib diisi!'),
product_price: Yup.number()
.required('Harga produk wajib diisi!')
.typeError('Harga produk wajib diisi!')
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
selling_price: Yup.number()
.required('Harga jual wajib diisi!')
.typeError('Harga jual wajib diisi!')
.min(0, 'Harga jual tidak boleh kurang dari 0!'),
tax: Yup.number()
.required('Pajak wajib diisi!')
.typeError('Pajak wajib diisi!')
.min(0, 'Pajak tidak boleh kurang dari 0!')
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!')
.typeError('Periode kadaluarsa wajib diisi!')
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
supplier_ids: Yup.array()
.of(Yup.number().required().typeError('Supplier tidak valid!'))
.min(1, 'Minimal harus ada 1 supplier!')
.required('Supplier wajib diisi!'),
flags: Yup.array()
.of(Yup.string().required())
.min(1, 'Minimal harus ada 1 flag!')
.required('Flag wajib diisi!'),
});
export const UpdateProductFormSchema = ProductFormSchema;
@@ -9,7 +9,11 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
@@ -79,20 +83,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
sku: initialValues?.sku ?? '',
uom: initialValues?.uom
? { value: initialValues.uom.id, label: initialValues.uom.name }
: null,
: undefined,
uom_id: initialValues?.uom?.id ?? 0,
product_category: initialValues?.product_category
? {
value: initialValues.product_category.id,
label: initialValues.product_category.name,
}
: null,
: undefined,
product_category_id: initialValues?.product_category?.id ?? 0,
product_price: initialValues?.product_price ?? 0,
selling_price: initialValues?.selling_price ?? 0,
tax: initialValues?.tax ?? 0,
expiry_period: initialValues?.expiry_period ?? 0,
supplier: null, // not used for payload, just for UI
product_price: initialValues?.product_price ?? '',
selling_price: initialValues?.selling_price ?? '',
tax: initialValues?.tax ?? '',
expiry_period: initialValues?.expiry_period ?? '',
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [],
flags: initialValues?.flags ?? [],
}),
@@ -111,16 +114,14 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
sku: values.sku,
uom_id: values.uom_id,
product_category_id: values.product_category_id,
product_price: values.product_price,
selling_price: values.selling_price,
tax: values.tax,
expiry_period: values.expiry_period,
supplier_ids: (values.supplier_ids ?? []).filter(
product_price: parseInt(values.product_price.toString()) || 0,
selling_price: parseInt(values.selling_price.toString()) || 0,
tax: parseInt(values.tax.toString()) || 0,
expiry_period: parseInt(values.expiry_period.toString()) || 0,
supplier_ids: values.supplier_ids.filter(
(id): id is number => typeof id === 'number'
),
flags: (values.flags ?? []).filter(
(f): f is string => typeof f === 'string'
),
flags: values.flags.filter((f): f is string => typeof f === 'string'),
};
switch (type) {
case 'add':
@@ -136,15 +137,11 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
const { setValues: formikSetValues } = formik;
// UOM
const [uomSelectInputValue, setUomSelectInputValue] = useState('');
const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ search: uomSelectInputValue ?? '' }).toString()}`;
const { data: uoms, isLoading: isLoadingUoms } = useSWR(
uomsUrl,
UomApi.getAllFetcher
);
const uomOptions = isResponseSuccess(uoms)
? uoms?.data.map((uom) => ({ value: uom.id, label: uom.name }))
: [];
const {
setInputValue: setUomSelectInputValue,
options: uomOptions,
isLoadingOptions: isLoadingUoms,
} = useSelect(UomApi.basePath, 'id', 'name');
const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('uom', true);
formik.setFieldValue('uom', val);
@@ -153,15 +150,11 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
};
// Product Category
const [categorySelectInputValue, setCategorySelectInputValue] = useState('');
const categoriesUrl = `${ProductCategoryApi.basePath}?${new URLSearchParams({ search: categorySelectInputValue ?? '' }).toString()}`;
const { data: categories, isLoading: isLoadingCategories } = useSWR(
categoriesUrl,
ProductCategoryApi.getAllFetcher
);
const categoryOptions = isResponseSuccess(categories)
? categories?.data.map((cat) => ({ value: cat.id, label: cat.name }))
: [];
const {
setInputValue: setCategorySelectInputValue,
options: categoryOptions,
isLoadingOptions: isLoadingCategories,
} = useSelect(ProductCategoryApi.basePath, 'id', 'name');
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('product_category', true);
formik.setFieldValue('product_category', val);
@@ -169,7 +162,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
formik.setFieldValue('product_category_id', (val as OptionType)?.value);
};
// Supplier (multi select)
// Supplier (multi select) - using SWR to filter by category
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`;
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
@@ -209,7 +202,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
return (
<>
<section className='w-full max-w-xl'>
<section className='w-full max-w-2xl'>
<header className='flex flex-col gap-4'>
<Button
href='/master-data/product'
@@ -235,7 +228,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
required
label='Nama'
name='name'
placeholder='Masukkan nama produk'
placeholder='Masukkan nama...'
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
@@ -247,7 +240,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
required
label='Merek'
name='brand'
placeholder='Masukkan merek produk'
placeholder='Masukkan merek...'
value={formik.values.brand}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
@@ -259,7 +252,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
required
label='SKU'
name='sku'
placeholder='Masukkan SKU produk'
placeholder='Masukkan SKU...'
value={formik.values.sku}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
@@ -270,6 +263,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<SelectInput
required
label='Satuan'
placeholder='Pilih satuan...'
value={formik.values.uom ?? undefined}
onChange={uomChangeHandler}
options={uomOptions}
@@ -283,6 +277,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<SelectInput
required
label='Kategori Produk'
placeholder='Pilih kategori produk...'
value={formik.values.product_category ?? undefined}
onChange={categoryChangeHandler}
options={categoryOptions}
@@ -296,15 +291,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
isDisabled={type === 'detail'}
isClearable
/>
<TextInput
<NumberInput
required
label='Harga Produk'
name='product_price'
type='number'
placeholder='Masukkan harga produk'
placeholder='Masukkan harga produk...'
value={formik.values.product_price}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputPrefix='Rp '
isError={
formik.touched.product_price &&
Boolean(formik.errors.product_price)
@@ -312,15 +311,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
errorMessage={formik.errors.product_price as string}
readOnly={type === 'detail'}
/>
<TextInput
<NumberInput
required
label='Harga Jual'
name='selling_price'
type='number'
placeholder='Masukkan harga jual'
placeholder='Masukkan harga jual...'
value={formik.values.selling_price}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputPrefix='Rp '
isError={
formik.touched.selling_price &&
Boolean(formik.errors.selling_price)
@@ -328,28 +331,36 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
errorMessage={formik.errors.selling_price as string}
readOnly={type === 'detail'}
/>
<TextInput
<NumberInput
required
label='Pajak (%)'
name='tax'
type='number'
placeholder='Masukkan pajak'
placeholder='Masukkan pajak...'
value={formik.values.tax}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputSuffix='%'
isError={formik.touched.tax && Boolean(formik.errors.tax)}
errorMessage={formik.errors.tax as string}
readOnly={type === 'detail'}
/>
<TextInput
<NumberInput
required
label='Periode Kadaluarsa (hari)'
name='expiry_period'
type='number'
placeholder='Masukkan periode kadaluarsa'
placeholder='Masukkan periode kadaluarsa...'
value={formik.values.expiry_period}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputSuffix='hari'
isError={
formik.touched.expiry_period &&
Boolean(formik.errors.expiry_period)
@@ -360,9 +371,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<SelectInput
required
label='Supplier'
placeholder='Pilih supplier...'
isMulti
value={supplierOptions.filter((opt) =>
formik.values.supplier_ids.includes(opt.value)
(formik.values.supplier_ids || []).includes(opt.value)
)}
onChange={supplierChangeHandler}
options={supplierOptions}
@@ -379,9 +391,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<SelectInput
required
label='Flags'
placeholder='Pilih flags...'
isMulti
value={PRODUCT_FLAG_OPTIONS.filter((opt) =>
formik.values.flags.includes(opt.value)
(formik.values.flags || []).includes(opt.value)
)}
onChange={(val) => {
const arr = Array.isArray(val) ? val : val ? [val] : [];
@@ -20,7 +20,6 @@ import { Icon } from '@iconify/react';
import { CellContext, SortingState } from '@tanstack/react-table';
import { useState } from 'react';
import useSWR from 'swr';
import ChickinForm from './form/ChickinForm';
const ChickinTable = () => {
const {
@@ -45,7 +45,7 @@ const ChickinFormKandang = ({
return (
<div className='flex flex-col gap-4'>
<FormHeader
title='Chick In DOC'
title={`Chick In ${initialValues.kandang?.name ?? 'Kandang'}`}
backUrl={`/production/project-flock/chickin/add?projectFlockId=${initialValues?.project_flock?.id}`}
/>
@@ -3,6 +3,7 @@ import Button from '@/components/Button';
import Card from '@/components/Card';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import PillBadge from '@/components/PillBadge';
import Table from '@/components/Table';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
@@ -31,12 +32,13 @@ const ChickinLogsView = ({
confirmModal.openModal();
};
const confirmationModalApproveClickHandler = async () => {
const confirmationModalApproveClickHandler = async (notes?: string) => {
setChickinErrorMessage('');
setIsApproveLoading(true);
const approveChickinRes = await ChickinApi.singleApproval(
initialValues?.id as number,
'APPROVED'
'APPROVED',
notes
);
if (isResponseSuccess(approveChickinRes)) {
toast.success(approveChickinRes?.message as string);
@@ -151,7 +153,7 @@ const ChickinLogsView = ({
</div>
)}
</Card>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={confirmModal.ref}
type='success'
text={`Apakah anda yakin ingin approve data Chickin yang Pending?`}
@@ -161,7 +163,9 @@ const ChickinLogsView = ({
primaryButton={{
text: 'Ya',
color: 'success',
onClick: confirmationModalApproveClickHandler,
onClick: (notes) => {
confirmationModalApproveClickHandler(notes);
},
isLoading: isApproveLoading,
}}
/>
@@ -8,8 +8,6 @@ import {
ChickinSchema,
} from '../ChickinForm.schema';
import DateInput from '@/components/input/DateInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import NumberInput from '@/components/input/NumberInput';
import Button from '@/components/Button';
import { useCallback, useEffect, useState } from 'react';
import { useFormik } from 'formik';
@@ -24,7 +22,6 @@ import Alert from '@/components/Alert';
import { formatNumber } from '@/lib/helper';
const ChickinFormView = ({
formType = 'add',
initialValues,
afterSubmit,
}: {
@@ -122,7 +119,7 @@ const ChickinFormView = ({
return (
<form
className='flex flex-col gap-4'
onReset={(e) => {
onReset={() => {
handleReset();
}}
onSubmit={formik.handleSubmit}
@@ -6,6 +6,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
@@ -144,6 +145,9 @@ const ProjectFlockTable = () => {
useState<ProjectFlock>();
const deleteModal = useModal();
const confirmModal = useModal();
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
'APPROVED'
);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
@@ -226,18 +230,21 @@ const ProjectFlockTable = () => {
updateFilter('search', e.target.value);
};
const confirmationModalApproveClickHandler = async () => {
const confirmApprovalHandler = async (
notes: string,
approvalAction: 'APPROVED' | 'REJECTED'
) => {
setIsApproveLoading(true);
const approveProjectFlockRes = await ProjectFlockApi.customRequest<
BaseApiResponse<ProjectFlock>,
ProjectFlockApprovalPayload
>(`/approvals`, {
method: 'POST',
payload: {
action: 'APPROVED',
approvable_ids: selectedRowIds.map((id) => id),
},
});
const approveProjectFlockRes =
approvalAction === 'APPROVED'
? await ProjectFlockApi.bulkApprove(
selectedRowIds.map((id) => id),
notes
)
: await ProjectFlockApi.bulkReject(
selectedRowIds.map((id) => id),
notes
);
if (isResponseSuccess(approveProjectFlockRes)) {
toast.success('Project Flock berhasil di-approve!');
@@ -271,6 +278,7 @@ const ProjectFlockTable = () => {
variant='outline'
color='success'
onClick={() => {
setApprovalAction('APPROVED');
confirmModal.openModal();
}}
disabled={selectedRowIds.length === 0}
@@ -279,6 +287,19 @@ const ProjectFlockTable = () => {
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<Button
variant='outline'
color='error'
onClick={() => {
setApprovalAction('REJECTED');
confirmModal.openModal();
}}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='mdi:times' width={24} height={24} />
Reject
</Button>
<div className='ms-auto w-full sm:w-auto'>
<DebouncedTextInput
name='search'
@@ -558,7 +579,7 @@ const ProjectFlockTable = () => {
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedProjectFlock?.flock?.name})?`}
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedProjectFlock?.flock_name})?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -570,17 +591,19 @@ const ProjectFlockTable = () => {
}}
/>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={confirmModal.ref}
type='success'
text={`Apakah anda yakin ingin approve data Project Flock ini (${selectedRowIds.length} data)?`}
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${approvalAction == 'APPROVED' ? 'approve' : 'reject'} data Project Flock ini (${selectedRowIds.length} data)?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'success',
onClick: confirmationModalApproveClickHandler,
color: approvalAction == 'APPROVED' ? 'success' : 'error',
onClick: (notes) => {
confirmApprovalHandler(notes, approvalAction);
},
isLoading: isApproveLoading,
}}
/>
@@ -14,13 +14,13 @@ import { cn } from '@/lib/helper';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { ProjectFlockKandangApi } from '@/services/api/production';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Kandang } from '@/types/api/master-data/kandang';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
import { FormHeader } from '@/components/helper/form/FormHeader';
const ProjectFlockChickinDetail = ({
projectFlockId,
@@ -42,10 +42,7 @@ const ProjectFlockChickinDetail = ({
const [projectFlock, setProjectFlock] = useState<ProjectFlock>();
// Fetch Data
const {
data: listProjectFlockKandang,
isLoading: isLoadingListProjectFlockKandang,
} = useSWR(
const { data: listProjectFlockKandang } = useSWR(
`${ProjectFlockKandangApi.basePath}?${new URLSearchParams({
search: searchProjectFlock,
project_flock_id:
@@ -104,6 +101,10 @@ const ProjectFlockChickinDetail = ({
}, [projectFlockId, listProjectFlock]);
return (
<>
<FormHeader
title={`Chick In ${projectFlock?.flock_name ?? 'Project Flock'}`}
backUrl='/production/project-flock'
/>
<div className='flex flex-col gap-4 w-full my-4'>
<div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'>
<SelectInput
@@ -118,7 +119,7 @@ const ProjectFlockChickinDetail = ({
value={
projectFlock
? {
label: `${projectFlock?.flock?.name}`,
label: `${projectFlock?.flock_name}`,
value: projectFlock?.id,
}
: null
@@ -175,7 +176,7 @@ const ProjectFlockChickinDetail = ({
},
{
header: 'Nama Flock',
accessorKey: 'flock.name',
accessorKey: 'flock_name',
},
{
header: 'Kategori',
@@ -209,10 +210,6 @@ const ProjectFlockChickinDetail = ({
);
},
},
{
header: 'Periode',
accessorKey: 'period',
},
{
header: 'FCR Layer',
accessorKey: 'fcr.name',
@@ -278,6 +275,10 @@ const ProjectFlockChickinDetail = ({
accessorKey: 'kandang.capacity',
header: 'Kapasitas',
},
{
accessorFn: () => projectFlock?.period,
header: 'Periode',
},
{
accessorKey: 'approval.step_name',
header: 'Status',
@@ -42,11 +42,6 @@ export const ProjectFlockFormSchema = Yup.object({
.min(1, 'Lokasi wajib diisi!')
.required('Lokasi wajib diisi!'),
period: Yup.number()
.required('Periode wajib diisi!')
.typeError('Periode harus berupa angka')
.min(1, 'Minimal periode adalah 1'),
kandang_ids: Yup.array()
.of(Yup.number().typeError('Kandang tidak valid!'))
.min(1, 'Minimal harus ada 1 kandang!')
@@ -29,7 +29,6 @@ import {
ProjectFlock,
} from '@/types/api/production/project-flock';
import toast from 'react-hot-toast';
import TextInput from '@/components/input/TextInput';
import { Kandang } from '@/types/api/master-data/kandang';
import Collapse from '@/components/Collapse';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
@@ -42,6 +41,7 @@ import ApprovalSteps, {
useApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
interface ProjectFlockFormProps {
formType?: 'add' | 'edit' | 'detail';
@@ -72,8 +72,11 @@ const ProjectFlockForm = ({
const [optionsKandang, setOptionsKandang] = useState<Kandang[]>(
initialValues?.kandangs ?? []
);
const [selectedFlock, setSelectedFlock] = useState<number | undefined>(
initialValues?.flock?.id ?? 0
const [selectedFlock, setSelectedFlock] = useState<string | undefined>(
initialValues?.flock_name?.slice(
0,
initialValues?.flock_name?.lastIndexOf(' ')
) ?? ''
);
const deleteModal = useModal();
@@ -102,9 +105,13 @@ const ProjectFlockForm = ({
useEffect(() => {
if (initialValues?.approval?.step_name) {
const approvedDisabled = initialValues.approval.step_name !== 'Pengajuan';
const pengajuanRejected =
initialValues.approval.step_number == 1 &&
initialValues.approval.action == 'REJECTED';
const approvedDisabled =
initialValues.approval.step_number !== 1 || pengajuanRejected;
setIsApprovedDisabled(approvedDisabled);
setIsRejectedDisabled(!approvedDisabled);
setIsRejectedDisabled(!approvedDisabled || pengajuanRejected);
setApprovalAction(!approvedDisabled ? 'APPROVED' : 'REJECTED');
}
}, [initialValues]);
@@ -143,15 +150,14 @@ const ProjectFlockForm = ({
mutate: refreshKandang,
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
const { data: periodFlocks, isLoading: isLoadingPeriodFlocks } = useSWR(
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
`${selectedFlock?.toString()}/periods`,
(id: string) => ProjectFlockApi.getNextPeriod(id)
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
);
const {
approvals,
isLoading: approvalsLoading,
rawDataApprovals: rawDataApprovals,
refresh: refreshApprovals,
} = useApprovalSteps({
latestApproval: initialValues?.approval,
@@ -182,6 +188,7 @@ const ProjectFlockForm = ({
formik.setFieldValue('kandang_ids', selectedRowIds);
}
}
refreshPeriodFlocks();
}
}, [kandang, selectedLocation]);
useEffect(() => {
@@ -278,13 +285,24 @@ const ProjectFlockForm = ({
// Formik InitialValue
const formikInitialValues = useMemo<ProjectFlockFormValues>(() => {
const trimFlock =
initialValues?.flock_name?.slice(
0,
initialValues?.flock_name?.lastIndexOf(' ')
) ?? '';
return {
name: initialValues?.flock_name,
flock: initialValues?.flock
flock: initialValues?.flock_name
? {
value: initialValues?.flock?.id ?? 0,
value:
optionsFlock.find((flock) => {
return flock.label == trimFlock;
})?.value ?? 0,
label:
initialValues?.flock?.name ?? initialValues?.flock_name ?? '',
formType != 'detail'
? (optionsFlock.find((flock) => {
return flock.label == trimFlock;
})?.label ?? '')
: initialValues?.flock_name,
}
: null,
area: initialValues?.area
@@ -311,31 +329,56 @@ const ProjectFlockForm = ({
label: initialValues.location.name,
}
: null,
flock_id: initialValues?.flock?.id ?? 0,
flock_name: initialValues?.flock_name ?? '',
flock_name:
optionsFlock.find((flock) => {
return (
flock.label ==
initialValues?.flock_name?.slice(
0,
initialValues?.flock_name?.lastIndexOf(' ')
)
);
})?.label ?? '',
area_id: initialValues?.area?.id ?? 0,
category: initialValues?.category as NonNullable<
'GROWING' | 'LAYING' | undefined
>,
fcr_id: initialValues?.fcr?.id ?? 0,
location_id: initialValues?.location?.id ?? 0,
period: initialValues?.period ?? 1,
kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
| number
| undefined
)[],
};
}, [initialValues]);
}, [initialValues, optionsFlock]);
// Formik
const formik = useFormik<ProjectFlockFormValues>({
initialValues: {
name: initialValues?.flock_name,
flock: initialValues?.flock
flock: initialValues?.flock_name
? {
value: initialValues?.flock?.id ?? 0,
value:
optionsFlock.find((flock) => {
return (
flock.label ==
initialValues?.flock_name?.slice(
0,
initialValues?.flock_name?.lastIndexOf(' ')
)
);
})?.value ?? 0,
label:
initialValues?.flock?.name ?? initialValues?.flock_name ?? '',
formType != 'detail'
? (optionsFlock.find((flock) => {
return (
flock.label ==
initialValues?.flock_name?.slice(
0,
initialValues?.flock_name?.lastIndexOf(' ')
)
);
})?.label ?? '')
: initialValues?.flock_name,
}
: null,
area: initialValues?.area
@@ -362,15 +405,24 @@ const ProjectFlockForm = ({
label: initialValues.location.name,
}
: null,
flock_id: initialValues?.flock?.id ?? 0,
flock_name: initialValues?.flock_name ?? '',
flock_name:
formType != 'detail'
? optionsFlock.find((flock) => {
return (
flock.label ==
initialValues?.flock_name?.slice(
0,
initialValues?.flock_name?.lastIndexOf(' ')
)
);
})?.label
: (initialValues?.flock_name ?? ''),
area_id: initialValues?.area?.id ?? 0,
category: initialValues?.category as NonNullable<
'GROWING' | 'LAYING' | undefined
>,
fcr_id: initialValues?.fcr?.id ?? 0,
location_id: initialValues?.location?.id ?? 0,
period: initialValues?.period ?? 1,
kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
| number
| undefined
@@ -385,12 +437,11 @@ const ProjectFlockForm = ({
onSubmit: async (values) => {
setProjectFlockFormErrorMessage('');
const payload: CreateProjectFlockPayload = {
flock_name: values.flock?.label as string,
flock_name: values.flock_name as string,
area_id: values.area_id as number,
category: values.category as string,
fcr_id: values.fcr_id as number,
location_id: values.location_id as number,
period: values.period as number,
kandang_ids: values.kandang_ids as number[],
};
@@ -419,8 +470,6 @@ const ProjectFlockForm = ({
if (initialValues?.area_id) {
setSelectedArea(initialValues?.area_id.toString() as string);
}
formik.setFieldValue('period', initialValues?.period);
}
}, [initialValues, setSelectedArea, formType]);
@@ -449,15 +498,6 @@ const ProjectFlockForm = ({
formik.validateForm();
}, [formik.values]);
useEffect(() => {
if (isResponseSuccess(periodFlocks)) {
formik.setFieldValue('period', periodFlocks.data.next_period);
}
if (isResponseError(periodFlocks)) {
console.log(periodFlocks?.message as string);
}
}, [periodFlocks]);
useEffect(() => {
const selectedRowIds = Object.keys(rowSelection)
.filter((id) => rowSelection[id])
@@ -485,32 +525,23 @@ const ProjectFlockForm = ({
setIsDeleteLoading(false);
};
const confirmationModalClickHandler = async ({
action = 'APPROVED',
}: {
action: 'APPROVED' | 'REJECTED';
}) => {
const confirmApprovalHandler = async (
notes: string,
approvalAction: 'REJECTED' | 'APPROVED'
) => {
if (initialValues?.id === undefined) return;
setIsApproveLoading(true);
const approveProjectFlockRes = await ProjectFlockApi.customRequest<
BaseApiResponse<ProjectFlock>,
ProjectFlockApprovalPayload
>(`/approvals`, {
method: 'POST',
payload: {
action: action,
approvable_ids: [initialValues?.id],
},
});
if (isResponseSuccess(approveProjectFlockRes)) {
if (refreshProjectFlocks) {
await refreshProjectFlocks();
}
toast.success(approveProjectFlockRes.message as string);
const approvalRes =
approvalAction == 'APPROVED'
? await ProjectFlockApi.approve(initialValues?.id, notes)
: await ProjectFlockApi.reject(initialValues?.id, notes);
if (isResponseSuccess(approvalRes)) {
refreshProjectFlocks?.();
toast.success(approvalRes.message as string);
}
if (isResponseError(approveProjectFlockRes)) {
toast.error(approveProjectFlockRes?.message as string);
if (isResponseError(approvalRes)) {
toast.error(approvalRes?.message as string);
}
refreshApprovals();
confirmModal.closeModal();
@@ -555,7 +586,7 @@ const ProjectFlockForm = ({
</div>
</div>
)}
{approvals && !approvalsLoading && (
{approvals && !approvalsLoading && formType == 'detail' && (
<ApprovalSteps approvals={approvals} />
)}
{formType == 'detail' && (
@@ -615,7 +646,6 @@ const ProjectFlockForm = ({
<div className='card bg-base-100 shadow w-full mb-6'>
<div className='card-body'>
<div className='card-title mb-4'>Informasi Umum</div>
<div className='grid sm:grid-cols-2 gap-4'>
<SelectInput
required
@@ -634,10 +664,19 @@ const ProjectFlockForm = ({
<SelectInput
required
label='Flock'
value={formik.values.flock as OptionType}
value={
formik.values.flock_name
? ({
label: formik.values.flock_name,
value: optionsFlock.find((flock) => {
return flock.label === formik.values.flock_name;
})?.value,
} as OptionType)
: undefined
}
onChange={(val) => {
optionChangeHandler(val, 'flock');
setSelectedFlock((val as OptionType)?.value as number);
setSelectedFlock((val as OptionType)?.label);
formik.setFieldValue(
'flock_name',
(val as OptionType)?.label
@@ -701,22 +740,6 @@ const ProjectFlockForm = ({
isClearable
isDisabled={formType === 'detail'}
/>
<TextInput
required
type='number'
name='period'
label='Periode'
placeholder='Masukkan periode yang project'
value={formik.values.period ?? (1 as number)}
onChange={formik.handleChange}
isError={
formik.touched.period && Boolean(formik.errors.period)
}
errorMessage={formik.errors.period as string}
readOnly={formType === 'detail'}
disabled={true}
isLoading={isLoadingPeriodFlocks}
/>
</div>
</div>
</div>
@@ -750,6 +773,9 @@ const ProjectFlockForm = ({
<span className='loading loading-dots loading-xl'></span>
)}
<ProjectFlockKandangTable
listPeriods={
isResponseSuccess(periodFlocks) ? periodFlocks.data : []
}
listKandang={optionsKandang}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
@@ -832,7 +858,7 @@ const ProjectFlockForm = ({
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${initialValues?.flock?.name} - ${initialValues?.area?.name})?`}
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${initialValues?.flock_name} - ${initialValues?.area?.name})?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -844,12 +870,12 @@ const ProjectFlockForm = ({
}}
/>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={confirmModal.ref}
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${
approvalAction == 'APPROVED' ? 'approve' : 'reject'
} Project Flock berikut? (${initialValues?.flock?.name} - ${
} Project Flock berikut? (${initialValues?.flock_name} - ${
initialValues?.area?.name
})?`}
secondaryButton={{
@@ -859,10 +885,8 @@ const ProjectFlockForm = ({
text: 'Ya',
color: approvalAction == 'APPROVED' ? 'success' : 'error',
isLoading: isApproveLoading,
onClick: () => {
confirmationModalClickHandler({
action: approvalAction,
});
onClick: (notes) => {
confirmApprovalHandler(notes, approvalAction);
},
}}
/>
@@ -5,10 +5,12 @@ import PillBadge from '@/components/PillBadge';
import Table from '@/components/Table';
import { cn } from '@/lib/helper';
import { Kandang } from '@/types/api/master-data/kandang';
import { ProjectFlockPeriods } from '@/types/api/production/project-flock';
import { OnChangeFn, Row } from '@tanstack/react-table';
import { useMemo } from 'react';
const ProjectFlockKandangTable = ({
listPeriods,
listKandang,
rowSelection,
setRowSelection,
@@ -16,6 +18,7 @@ const ProjectFlockKandangTable = ({
initialValues,
formType = 'add',
}: {
listPeriods: ProjectFlockPeriods;
listKandang: Kandang[];
rowSelection: Record<string, boolean>;
setRowSelection: OnChangeFn<Record<string, boolean>>;
@@ -134,6 +137,19 @@ const ProjectFlockKandangTable = ({
accessorFn: (row) => row.capacity,
header: 'Kapasitas',
},
{
accessorFn: (row) => row.location?.name,
header: 'Periode',
cell: (props) => {
console.log('listPeriods');
console.log(listPeriods);
const period =
listPeriods.length > 0
? listPeriods.find((p) => p.id == props.row.original.id)
: undefined;
return period?.period ?? '-';
},
},
{
accessorFn: (row) => row.pic?.name,
header: 'Penanggung Jawab',
File diff suppressed because it is too large Load Diff
@@ -1,212 +1,320 @@
import * as Yup from 'yup';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import { Recording } from '@/types/api/production/recording';
import {
Recording,
CreateGrowingRecordingPayload,
CreateLayingRecordingPayload,
CreateEggPayload,
CreateGradingPayload,
} from '@/types/api/production/recording';
export const RecordingFormSchema = Yup.object({
flock: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
flock_id: Yup.number()
.default(0)
.typeError('Flock wajib diisi!')
.test(
'is-valid-flock',
'Flock wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Flock wajib diisi!'),
location: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
location_id: Yup.number()
.default(0)
.typeError('Lokasi wajib diisi!')
.test(
'is-valid-location',
'Lokasi wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Lokasi wajib diisi!'),
coop: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
coop_id: Yup.number()
.default(0)
.typeError('Kandang wajib diisi!')
.test(
'is-valid-coop',
'Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Kandang wajib diisi!'),
recording_date: Yup.date()
.required('Tanggal recording wajib diisi')
.typeError('Format tanggal tidak valid'),
feed_data: Yup.array()
.of(
Yup.object({
feed_id: Yup.string().required('Nama pakan wajib diisi!'),
feed_qty: Yup.mixed<number | ''>().notRequired(),
feed_stock: Yup.number()
.required('Jumlah pakan yang digunakan wajib diisi!')
.min(1, 'Jumlah pakan minimal 1!')
.typeError('Jumlah pakan yang digunakan harus berupa angka!')
.test(
'is-not-exceed-qty',
'Jumlah pakan yang digunakan tidak boleh melebihi stok tersedia!',
function (value) {
const { feed_qty } = this.parent;
if (value === undefined) return true;
if (
feed_qty === undefined ||
feed_qty === '' ||
typeof feed_qty !== 'number'
)
return true;
return value <= feed_qty;
}
),
})
)
.min(1, 'Minimal harus ada 1 data pakan!')
.required('Data pakan wajib diisi!'),
body_weight: Yup.array()
.of(
Yup.object({
chicken_weight: Yup.number()
.required('Berat ayam wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'),
chicken_count: Yup.number()
.required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!'),
average_chicken_weight: Yup.number()
.required('Rata-rata berat ayam wajib diisi!')
.min(1, 'Rata-rata berat ayam minimal 1 gram!')
.typeError('Rata-rata berat ayam harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
vaccination: Yup.array()
.of(
Yup.object({
vaccine_id: Yup.string().required('Nama vaksin wajib diisi!'),
total_stock: Yup.mixed<number | ''>().notRequired(),
used_stock: Yup.number()
.required('Jumlah vaksin yang digunakan wajib diisi!')
.min(1, 'Jumlah vaksin minimal 1!')
.typeError('Jumlah vaksin yang digunakan harus berupa angka!')
.test(
'is-not-exceed-total',
'Jumlah vaksin yang digunakan tidak boleh melebihi stok tersedia!',
function (value) {
const { total_stock } = this.parent;
if (value === undefined) return true;
if (
total_stock === undefined ||
total_stock === '' ||
typeof total_stock !== 'number'
)
return true;
return value <= total_stock;
}
),
})
)
.min(1, 'Minimal harus ada 1 data vaksinasi!')
.required('Data vaksinasi wajib diisi!'),
mortality: Yup.array()
.of(
Yup.object({
condition: Yup.mixed<string>()
.oneOf(
RECORDING_FLAG_OPTIONS.map((opt) => opt.value),
'Kondisi tidak valid!'
)
.required('Kondisi wajib diisi!'),
count: Yup.number()
.required('Jumlah mortalitas wajib diisi!')
.min(1, 'Jumlah mortalitas minimal 1 ekor!')
.typeError('Jumlah mortalitas harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data mortalitas!')
.required('Data mortalitas wajib diisi!'),
type RecordingGrowingFormSchemaType = {
project_flock_kandang: {
value: number;
label: string;
} | null;
project_flock_kandang_id: number;
body_weights: {
weight: number | string;
avg_weight: number | string;
qty: number | string;
}[];
stocks: {
product_warehouse_id: number;
qty: number | string;
}[];
depletions: {
product_warehouse_id: number;
qty: number | string;
}[];
};
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
eggs: {
product_warehouse_id: number;
qty: number | string;
}[];
};
type RecordingGradingFormSchemaType = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number | string;
}[];
};
export type BodyWeightSchema = {
weight: number | string;
avg_weight: number | string;
qty: number | string;
};
export type StockSchema = {
product_warehouse_id: number;
qty: number | string;
};
export type DepletionSchema = {
product_warehouse_id: number;
qty: number | string;
};
export type EggSchema = {
product_warehouse_id: number;
qty: number | string;
};
const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({
weight: Yup.number()
.required('Berat ayam total wajib diisi!')
.min(1, 'Berat ayam total minimal 1 gram!')
.typeError('Berat ayam total harus berupa angka!'),
avg_weight: Yup.number()
.required('Berat ayam rata-rata wajib diisi!')
.typeError('Berat ayam rata-rata harus berupa angka!'),
qty: Yup.number()
.required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!'),
});
export const UpdateRecordingFormSchema = RecordingFormSchema;
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
product_warehouse_id: Yup.number()
.required('Produk wajib diisi!')
.min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'),
qty: Yup.number()
.required('Jumlah penggunaan wajib diisi!')
.min(1, 'Jumlah penggunaan tidak boleh 0!')
.typeError('Jumlah penggunaan harus berupa angka!'),
});
export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>;
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
product_warehouse_id: Yup.number()
.required('Produk depletions wajib diisi!')
.min(1, 'Produk depletions wajib diisi!')
.typeError('Produk depletions harus berupa angka!'),
qty: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
});
export const getRecordingFormInitialValues = (
initialValues?: Recording
): RecordingFormValues => ({
flock: initialValues?.flock
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
product_warehouse_id: Yup.number()
.required('Kondisi telur wajib diisi!')
.min(1, 'Kondisi telur wajib diisi!')
.typeError('Kondisi telur harus berupa angka!'),
qty: Yup.number()
.required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur tidak boleh 0!')
.typeError('Jumlah telur harus berupa angka!'),
});
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
Yup.object({
project_flock_kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
project_flock_kandang_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Project Flock Kandang wajib diisi!')
.test(
'not-already-recorded',
'Project Flock ini sudah direcord hari ini!',
function (value) {
const recordedProjectFlockIds = this.options.context
?.recordedProjectFlockIds as Set<number>;
const formType = this.options.context?.type as
| 'add'
| 'edit'
| 'detail';
if (formType !== 'add') return true;
if (value && recordedProjectFlockIds?.has(value)) {
return false;
}
return true;
}
),
body_weights: Yup.array()
.of(BodyWeightObjectSchema)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
stocks: Yup.array()
.of(StockObjectSchema)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array()
.of(DepletionObjectSchema)
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
});
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
RecordingGrowingFormSchema.shape({
eggs: Yup.array()
.of(EggObjectSchema)
.min(1, 'Minimal harus ada 1 data telur!')
.required('Data telur wajib diisi!'),
});
export const UpdateRecordingGrowingFormSchema =
RecordingGrowingFormSchema.shape({
project_flock_kandang_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Project Flock Kandang wajib diisi!'),
});
export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
project_flock_kandang_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Project Flock Kandang wajib diisi!'),
});
export const RecordingGradingFormSchema: Yup.ObjectSchema<RecordingGradingFormSchemaType> =
Yup.object({
eggs_grading: Yup.array()
.of(
Yup.object({
recording_egg_id: Yup.number()
.required('Recording Egg ID wajib diisi!')
.min(1, 'Recording Egg ID minimal 1!')
.typeError('Recording Egg ID harus berupa angka!'),
grade: Yup.string()
.required('Grade telur wajib diisi!')
.typeError('Grade telur harus berupa string!'),
qty: Yup.number()
.required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur minimal 1!')
.typeError('Jumlah telur harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data grading telur!')
.required('Data grading telur wajib diisi!'),
});
export const UpdateRecordingGradingFormSchema = RecordingGradingFormSchema;
export type RecordingGrowingFormValues = Yup.InferType<
typeof RecordingGrowingFormSchema
>;
export type RecordingLayingFormValues = Yup.InferType<
typeof RecordingLayingFormSchema
>;
export type RecordingGradingFormValues = Yup.InferType<
typeof RecordingGradingFormSchema
>;
type RecordingFormData = Partial<Recording> & {
body_weights?: CreateGrowingRecordingPayload['body_weights'];
stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks'];
depletions?:
| CreateGrowingRecordingPayload['depletions']
| Recording['depletions'];
eggs?: CreateLayingRecordingPayload['eggs'] | Recording['eggs'];
project_flock_kandang_id?: number;
project_flock_category?: string;
};
export const getRecordingGrowingFormInitialValues = (
initialValues?: RecordingFormData
): RecordingGrowingFormValues => ({
project_flock_kandang: initialValues?.project_flock_kandang_id
? {
value: initialValues.flock.id,
label: initialValues.flock.name,
value: initialValues.project_flock_kandang_id,
label: `Project Flock #${initialValues.project_flock_kandang_id}`,
}
: null,
flock_id: initialValues?.flock?.id ?? 0,
location: initialValues?.location
? {
value: initialValues.location.id,
label: initialValues.location.name,
}
: null,
location_id: initialValues?.location?.id ?? 0,
coop: initialValues?.coop
? {
value: initialValues.coop.id,
label: initialValues.coop.name,
}
: null,
coop_id: initialValues?.coop?.id ?? 0,
recording_date: initialValues?.recording_date
? new Date(initialValues.recording_date)
: new Date(),
feed_data: initialValues?.feed_data
? initialValues.feed_data.map((feed) => ({
feed_id: feed.feed_name,
feed_qty: feed.feed_qty,
feed_stock: feed.feed_stock,
}))
: [
{
feed_id: '',
feed_qty: '',
feed_stock: 0,
},
],
body_weight: initialValues?.body_weight ?? [
project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0,
body_weights: initialValues?.body_weights?.map(
(bw: NonNullable<CreateGrowingRecordingPayload['body_weights']>[0]) => ({
weight: bw.avg_weight * bw.qty,
avg_weight: bw.avg_weight,
qty: bw.qty,
})
) ?? [
{
chicken_weight: 0,
chicken_count: 0,
average_chicken_weight: 0,
weight: '',
avg_weight: '',
qty: '',
},
],
vaccination: initialValues?.vaccination
? initialValues.vaccination.map((vaccine) => ({
vaccine_id: vaccine.vaccine_name,
total_stock: vaccine.total_stock,
used_stock: vaccine.used_stock,
}))
: [
{
vaccine_id: '',
total_stock: '',
used_stock: 0,
},
],
mortality: initialValues?.mortality ?? [
stocks: initialValues?.stocks?.map((stock) => ({
product_warehouse_id: stock.product_warehouse_id,
qty:
(stock as { qty?: number; usage_amount?: number }).qty ||
(stock as { qty?: number; usage_amount?: number }).usage_amount ||
'',
})) ?? [
{
condition: '',
count: 0,
product_warehouse_id: 0,
qty: '',
},
],
depletions: initialValues?.depletions?.map(
(
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
) => ({
product_warehouse_id: depletion.product_warehouse_id,
qty: depletion.qty,
})
) ?? [
{
product_warehouse_id: 0,
qty: '',
},
],
});
export const getRecordingLayingFormInitialValues = (
initialValues?: RecordingFormData
): RecordingLayingFormValues => ({
...getRecordingGrowingFormInitialValues(initialValues),
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
product_warehouse_id: egg.product_warehouse_id,
qty: egg.qty,
})) ?? [
{
product_warehouse_id: 0,
qty: '',
},
],
});
export const getRecordingGradingFormInitialValues = (
initialValues?: Partial<CreateGradingPayload> & { recording_egg_id?: number }
): RecordingGradingFormValues => ({
eggs_grading: initialValues?.eggs_grading?.map((grading) => ({
recording_egg_id: grading.recording_egg_id,
grade: grading.grade,
qty: grading.qty,
})) ?? [
{
recording_egg_id: initialValues?.recording_egg_id ?? 0,
grade: '',
qty: '',
},
],
});
File diff suppressed because it is too large Load Diff
@@ -1,70 +0,0 @@
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
import { useModal } from '@/components/Modal';
import { RecordingApi } from '@/services/api/production';
import {
CreateRecordingPayload,
UpdateRecordingPayload,
} from '@/types/api/production/recording';
import { isResponseError } from '@/lib/api-helper';
export const useRecordingFormHandlers = (initialValuesId?: number) => {
const router = useRouter();
const deleteModal = useModal();
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const createRecordingHandler = useCallback(
async (payload: CreateRecordingPayload) => {
const res = await RecordingApi.create(payload);
if (isResponseError(res)) {
setRecordingFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.push('/flock/recording');
},
[router]
);
const updateRecordingHandler = useCallback(
async (recordingId: number, payload: UpdateRecordingPayload) => {
const res = await RecordingApi.update(recordingId, payload);
if (res?.status === 'error') {
setRecordingFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.refresh();
router.push('/flock/recording');
},
[router]
);
const deleteRecordingClickHandler = useCallback(() => {
deleteModal.openModal();
}, [deleteModal]);
const confirmationModalDeleteClickHandler = useCallback(async () => {
if (!initialValuesId) return;
setIsDeleteLoading(true);
await RecordingApi.delete(initialValuesId);
deleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false);
router.push('/flock/recording');
}, [deleteModal, initialValuesId, router]);
return {
deleteModal,
recordingFormErrorMessage,
isDeleteLoading,
createRecordingHandler,
updateRecordingHandler,
deleteRecordingClickHandler,
confirmationModalDeleteClickHandler,
};
};
File diff suppressed because it is too large Load Diff
+60
View File
@@ -33,6 +33,66 @@ export const TRANSFER_TO_LAYING_APPROVAL_LINE: ApprovalLine = [
},
] as const;
export const MARKETING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Pengajuan',
},
{
step_number: 2,
step_name: 'Sales Order',
},
{
step_number: 3,
step_name: 'Delivery Order',
},
] as const;
export const RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_name: 'Disetujui',
},
] as const;
export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_name: 'Disetujui',
},
] as const;
export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_name: 'Disetujui',
},
] as const;
export const EXPENSE_REQUEST_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
+37 -4
View File
@@ -48,7 +48,7 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
{
title: 'Penjualan',
link: '/marketing/sales-orders',
link: '/marketing',
icon: 'mdi:attach-money',
},
@@ -233,9 +233,42 @@ export const SUPPLIER_FLAG_OPTIONS = [
];
export const RECORDING_FLAG_OPTIONS = [
{ label: 'Ayam Afkir', value: 'Ayam Afkir' },
{ label: 'Ayam Culling', value: 'Ayam Culling' },
{ label: 'Ayam Mati', value: 'Ayam Mati' },
{ label: 'Ayam Afkir', value: 'Afkir' },
{ label: 'Ayam Culling', value: 'Culling' },
{ label: 'Ayam Mati', value: 'Mati' },
];
export const APPROVAL_WORKFLOWS = [
{
key: 'PROJECT_FLOCKS',
steps: [
{
step_number: 1,
step_name: 'Pengajuan',
},
{
step_number: 2,
step_name: 'Aktif',
},
],
},
{
key: 'RECORDINGS',
steps: [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_name: 'Disetujui',
},
],
},
];
export const ACCEPTED_FILE_TYPE = {
+189 -165
View File
@@ -4,12 +4,34 @@ import { Location } from '@/types/api/master-data/location';
import { Kandang } from '@/types/api/master-data/kandang';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Marketing } from '@/types/api/marketing/marketing';
import { CreatedUser } from '@/types/api/api-general';
import {
BaseMarketing,
Marketing,
BaseSalesOrder,
BaseDeliveryOrder,
BaseDelivery,
} from '@/types/api/marketing/marketing';
import {
CreatedUser,
BaseApproval,
BaseMetadata,
} from '@/types/api/api-general';
import { Product } from '@/types/api/master-data/product';
import { Customer } from '@/types/api/master-data/customer';
import { Uom } from '@/types/api/master-data/uom';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { Supplier } from '@/types/api/master-data/supplier';
// Waktu saat ini untuk created_at/updated_at
const now = format(new Date(), 'yyyy-MM-dd HH:mm:ss');
const today = format(new Date(), 'yyyy-MM-dd');
const tomorrow = format(
new Date().setDate(new Date().getDate() + 1),
'yyyy-MM-dd'
);
// ======================
// 👤 Created User
// 👤 Created User & Helper Data
// ======================
export const createdUser: CreatedUser = {
id: 1,
@@ -18,6 +40,24 @@ export const createdUser: CreatedUser = {
name: 'Admin Utama',
};
const dummyProductBase: Product = {
id: 101,
name: 'Pakan Ayam Premium',
brand: 'Brand Hebat',
sku: 'PAK-001',
product_price: 15000,
selling_price: 18000,
tax: 0.1,
expiry_period: 365,
uom: { id: 1, name: 'Sak' } as Uom,
product_category: { id: 1, name: 'Pakan' } as ProductCategory,
suppliers: [{ id: 1, name: 'Supplier A' } as Supplier],
flags: ['PAKAN'],
created_user: createdUser,
created_at: now,
updated_at: now,
};
// ======================
// 📍 Area Dummy
// ======================
@@ -26,15 +66,15 @@ export const dummyAreas: Area[] = [
id: 1,
name: 'Bandung Barat',
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
created_at: now,
updated_at: now,
},
{
id: 2,
name: 'Cimahi Utara',
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
created_at: now,
updated_at: now,
},
];
@@ -48,8 +88,8 @@ export const dummyLocations: Location[] = [
address: 'Jl. Sukajadi No. 12',
area: dummyAreas[0],
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
created_at: now,
updated_at: now,
},
{
id: 2,
@@ -57,8 +97,8 @@ export const dummyLocations: Location[] = [
address: 'Jl. Setiabudi No. 45',
area: dummyAreas[1],
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
created_at: now,
updated_at: now,
},
];
@@ -74,8 +114,8 @@ export const dummyKandangs: Kandang[] = [
location: dummyLocations[0],
pic: createdUser,
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
created_at: now,
updated_at: now,
},
{
id: 2,
@@ -85,8 +125,8 @@ export const dummyKandangs: Kandang[] = [
location: dummyLocations[1],
pic: createdUser,
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
created_at: now,
updated_at: now,
},
];
@@ -100,9 +140,9 @@ export const dummyWarehouses: Warehouse[] = [
name: 'Gudang Wilayah Bandung Barat',
area: dummyAreas[0],
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
created_at: now,
updated_at: now,
} as Warehouse,
{
id: 2,
type: 'LOKASI',
@@ -110,9 +150,9 @@ export const dummyWarehouses: Warehouse[] = [
area: dummyAreas[0],
location: { ...dummyLocations[0], area: dummyAreas[0] },
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
created_at: now,
updated_at: now,
} as Warehouse,
{
id: 3,
type: 'KANDANG',
@@ -125,9 +165,9 @@ export const dummyWarehouses: Warehouse[] = [
pic: createdUser,
},
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
created_at: now,
updated_at: now,
} as Warehouse,
];
// ======================
@@ -139,16 +179,11 @@ export const dummyProductWarehouses: ProductWarehouse[] = [
product_id: 101,
warehouse_id: 1,
quantity: 1000,
product: {
id: 101,
name: 'Pakan Ayam Premium',
sku: 'PAK-001',
category: 'PAKAN',
} as unknown as Product,
product: dummyProductBase,
warehouse: dummyWarehouses[0],
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
created_at: now,
updated_at: now,
},
{
id: 2,
@@ -156,29 +191,95 @@ export const dummyProductWarehouses: ProductWarehouse[] = [
warehouse_id: 2,
quantity: 500,
product: {
...dummyProductBase,
id: 102,
name: 'Vitamin Ayam Super',
sku: 'VIT-002',
category: 'VITAMIN',
} as unknown as Product,
flags: ['VITAMIN'],
selling_price: 25000,
},
warehouse: dummyWarehouses[1],
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
created_at: now,
updated_at: now,
},
];
// ======================
// 💼 Marketing Dummy
// ======================
export const dummyMarketings: Marketing[] = [
// Step 1: Pengajuan Order
// Helper untuk Sales Order (SO) Item
const soItem1: BaseSalesOrder = {
vehicle_number: 'B 1234 ABC',
id: 101,
marketing_id: 1,
product_warehouse_id: 1,
qty: 100,
unit_price: 18000, // Harga jual
avg_weight: 1.0,
total_weight: 100 * 1.0,
total_price: 100 * 18000,
product_warehouse: dummyProductWarehouses[0] as ProductWarehouse,
};
const soItem2: BaseSalesOrder = {
vehicle_number: 'D 5678 EFG',
id: 102,
marketing_id: 2,
product_warehouse_id: 2,
qty: 50,
unit_price: 25000,
avg_weight: 0.5,
total_weight: 50 * 0.5,
total_price: 50 * 25000,
product_warehouse: dummyProductWarehouses[1] as ProductWarehouse,
};
// Helper untuk Delivery Item (DO) Detail
const doDelivery1: BaseDelivery[] = [
{
product_warehouse: dummyProductWarehouses[0] as ProductWarehouse,
qty: soItem1.qty,
unit_price: soItem1.unit_price,
total_weight: soItem1.total_weight,
avg_weight: soItem1.avg_weight,
total_price: soItem1.total_price,
vehicle_number: 'B 1234 ABC',
},
];
const doDelivery2: BaseDelivery[] = [
{
product_warehouse: dummyProductWarehouses[1] as ProductWarehouse,
qty: soItem2.qty,
unit_price: soItem2.unit_price,
total_weight: soItem2.total_weight,
avg_weight: soItem2.avg_weight,
total_price: soItem2.total_price,
vehicle_number: 'D 5678 EFG',
},
];
// Helper untuk Delivery Order (DO) Header
const deliveryOrder1: BaseDeliveryOrder[] = [
{
id: 1,
status: 'APPROVED',
marketing_id: 3,
do_number: 'DO-003-2025',
delivery_date: tomorrow,
warehouse: dummyWarehouses[0],
deliveries: doDelivery1,
},
];
export const dummyMarketings: Marketing[] = [
// 1. Pengajuan Order (Langkah Pertama/Awal)
{
id: 1,
status: 'DRAFT',
// name: 'SO-001-2025', // `name` is not part of BaseMarketing
so_number: 'SO-001-2025',
so_docs: 'https://example.com/docs/so001.pdf',
so_date: format(new Date(), 'yyyy-MM-dd'),
so_date: today,
customer: {
id: 1,
name: 'PT Maju Jaya',
@@ -190,52 +291,32 @@ export const dummyMarketings: Marketing[] = [
email: 'contact@majujaya.com',
account_number: '1234567890',
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
created_at: now,
updated_at: now,
} as Customer,
sales_person: createdUser,
notes: 'Pengiriman awal bulan.',
grand_total: 7500000,
approval: {
notes: 'Pengajuan Order Awal, menunggu persetujuan harga.',
latest_approval: {
step_number: 1,
step_name: 'Pengajuan Order',
action: 'APPROVED',
action: 'CREATED',
action_by: createdUser,
action_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
marketing_products: [
{
id: 1,
qty: 100,
unit_price: 75000,
avg_weight: 2.5,
total_weight: 250,
total_price: 7500000,
product_warehouse: dummyProductWarehouses[0],
marketing_delivery_products: {
id: 1,
qty: 100,
unit_price: 75000,
avg_weight: 2.5,
total_weight: 250,
total_price: 7500000,
delivery_date: format(new Date(), 'yyyy-MM-dd'),
vehicle_number: 'B 1234 XY',
},
},
],
action_at: now,
} as BaseApproval,
sales_order: [soItem1],
delivery_order: [],
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
created_at: now,
updated_at: now,
} as Marketing,
// Step 2: Sales Order
// 2. Sales Order (Disetujui dan Siap DO)
{
id: 2,
status: 'APPROVED',
// name: 'SO-002-2025', // `name` is not part of BaseMarketing
so_number: 'SO-002-2025',
so_docs: 'https://example.com/docs/so002.pdf',
so_date: format(new Date(), 'yyyy-MM-dd'),
so_date: today,
customer: {
id: 2,
name: 'CV Sumber Sehat',
@@ -247,52 +328,33 @@ export const dummyMarketings: Marketing[] = [
email: 'info@sumbersehat.com',
account_number: '9876543210',
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
created_at: now,
updated_at: now,
} as Customer,
sales_person: createdUser,
notes: 'Pesanan kedua untuk stok akhir tahun.',
grand_total: 3750000,
approval: {
notes: 'Sales Order telah disetujui oleh Supervisor.',
latest_approval: {
id: 2,
step_number: 2,
step_name: 'Sales Order',
action: 'APPROVED',
action_by: createdUser,
action_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
marketing_products: [
{
id: 2,
qty: 50,
unit_price: 75000,
avg_weight: 2.5,
total_weight: 125,
total_price: 3750000,
product_warehouse: dummyProductWarehouses[1],
marketing_delivery_products: {
id: 2,
qty: 50,
unit_price: 75000,
avg_weight: 2.5,
total_weight: 125,
total_price: 3750000,
delivery_date: format(new Date(), 'yyyy-MM-dd'),
vehicle_number: 'B 5678 YZ',
},
},
],
action_at: now,
} as BaseApproval,
sales_order: [soItem2],
delivery_order: [], // Belum ada pengiriman (DO) yang dibuat
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
created_at: now,
updated_at: now,
} as Marketing,
// Step 3: Delivery Order
// 3. Delivery Order (Proses Pengiriman telah dibuat)
{
id: 3,
status: 'APPROVED',
status: 'DELIVERED', // Asumsi status DELIVERED berarti DO sudah selesai/terbuat
// name: 'SO-003-2025', // `name` is not part of BaseMarketing
so_number: 'SO-003-2025',
so_docs: 'https://example.com/docs/so003.pdf',
so_date: format(new Date(), 'yyyy-MM-dd'),
so_date: today,
customer: {
id: 3,
name: 'UD Ternak Sejahtera',
@@ -304,61 +366,23 @@ export const dummyMarketings: Marketing[] = [
email: 'halo@ternaksejahtera.com',
account_number: '1122334455',
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
created_at: now,
updated_at: now,
} as Customer,
sales_person: createdUser,
notes: 'Order untuk pengiriman ke luar kota.',
grand_total: 5600000,
approval: {
notes: 'Pengiriman barang telah berhasil dilakukan.',
latest_approval: {
id: 3,
step_number: 3,
step_name: 'Delivery Order',
action: 'APPROVED',
action: 'COMPLETED',
action_by: createdUser,
action_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
marketing_products: [
{
id: 3,
qty: 80,
unit_price: 70000,
avg_weight: 2.4,
total_weight: 192,
total_price: 5600000,
product_warehouse: dummyProductWarehouses[0],
marketing_delivery_products: {
id: 3,
qty: 80,
unit_price: 70000,
avg_weight: 2.4,
total_weight: 192,
total_price: 5600000,
delivery_date: format(new Date(), 'yyyy-MM-dd'),
vehicle_number: 'D 9090 ZZ',
},
},
{
id: 4,
qty: 80,
unit_price: 70000,
avg_weight: 2.4,
total_weight: 192,
total_price: 5600000,
product_warehouse: dummyProductWarehouses[0],
marketing_delivery_products: {
id: 3,
qty: 80,
unit_price: 70000,
avg_weight: 2.4,
total_weight: 192,
total_price: 5600000,
delivery_date: format(new Date(), 'yyyy-MM-dd'),
vehicle_number: 'D 9090 ZZ',
},
},
],
action_at: now,
} as BaseApproval,
sales_order: [soItem1, soItem2],
delivery_order: deliveryOrder1, // DO sudah terbuat
created_user: createdUser,
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
},
created_at: now,
updated_at: now,
} as Marketing,
];
+1 -2
View File
@@ -2,7 +2,6 @@ import moment from 'moment';
import 'moment/locale/id';
import { twMerge } from 'tailwind-merge';
import clsx, { ClassValue } from 'clsx';
import { ChangeEvent } from 'react';
// set locale globally
moment.locale('id');
@@ -32,7 +31,7 @@ export const formatNumber = (
export function formatVechicleNumber(value: string): string {
let result = '';
for (let i = 0; i < value.length; i++) {
for (let i = 0; i < (value?.length ?? 0); i++) {
const curr = value[i];
const prev = value[i - 1];
+6
View File
@@ -0,0 +1,6 @@
import { BaseApiService } from '@/services/api/base';
import { BaseApproval } from '@/types/api/api-general';
export const ApprovalApi = new BaseApiService<BaseApproval, unknown, unknown>(
'/approvals'
);
+1 -2
View File
@@ -7,7 +7,6 @@ import {
import {
CreateMovementPayload,
Movement,
UpdateMovementPayload,
} from '@/types/api/inventory/movement';
import {
CreateInventoryAdjustmentPayload,
@@ -23,7 +22,7 @@ export const ProductWarehouseApi = new BaseApiService<
export const MovementApi = new BaseApiService<
Movement,
CreateMovementPayload,
UpdateMovementPayload
unknown
>('/inventory/transfers');
export const inventoryAdjustmentApi = new BaseApiService<
+87 -63
View File
@@ -5,82 +5,74 @@ import { httpClient } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import {
Marketing,
CreateMarketingPayload,
UpdateMarketingPayload,
CreateSalesOrderPayload,
UpdateSalesOrderPayload,
CreateDeliveryOrderPayload,
UpdateDeliveryOrderPayload,
} from '@/types/api/marketing/marketing';
export class MarketingService extends BaseApiService<
/**
* 💡 Helper untuk membuat respons dummy
* @param data Data yang akan dimasukkan ke dalam body respons
*/
const createDummyResponse = <T>(data: T): BaseApiResponse<T> => ({
code: 200,
status: 'success',
message: 'Data retrieved successfully (MOCK)',
data: data,
});
export class SalesOrderService extends BaseApiService<
Marketing,
CreateMarketingPayload,
UpdateMarketingPayload
CreateSalesOrderPayload,
UpdateSalesOrderPayload
> {
constructor(basePath: string = '/marketing') {
super(basePath);
}
/**
* Override: Get all marketing data (dummy mode)
*/
override async getAllFetcher(
endpoint: string
): Promise<BaseApiResponse<Marketing[]>> {
// simulasi loading
await sleep(750);
// /**
// * Override: Mengambil semua data Marketing dari dummyMarketings
// */
// async getAllFetcher(endpoint: string): Promise<BaseApiResponse<Marketing[]>> {
// // Simulasi delay jaringan
// await sleep(500);
// data dummy sementara
const DUMMY_MARKETING_DATA: BaseApiResponse<Marketing[]> = {
code: 200,
status: 'success',
message: 'Berhasil mengambil data marketing (dummy)',
data: dummyMarketings,
};
// // Filter data marketing yang valid (jika menggunakan BaseMarketing[])
// const data = dummyMarketings as Marketing[];
return DUMMY_MARKETING_DATA;
}
// return createDummyResponse<Marketing[]>(data);
// }
/**
* Override: Get single marketing data (dummy mode)
*/
override async getSingle(
id: number
): Promise<BaseApiResponse<Marketing> | undefined> {
// simulasi delay
await new Promise((res) => setTimeout(res, 500));
// /**
// * Override: Mengambil satu data Marketing berdasarkan ID dari dummyMarketings
// */
// async getSingle(id: number): Promise<BaseApiResponse<Marketing> | undefined> {
// // Simulasi delay jaringan
// await sleep(300);
const marketing = dummyMarketings.find((marketing) => {
console.log('marketing', marketing);
console.log('id-m', marketing.id);
console.log('id-p', id);
console.log('id', marketing.id == id);
return marketing.id == id;
});
console.log('marketings', dummyMarketings);
console.log('marketing', marketing);
// const foundData = dummyMarketings.find((m) => m.id == id);
if (marketing) {
// misalnya fetch dari dummy
return {
code: 200,
status: 'success',
message: 'Data marketing berhasil diambil.',
data: marketing,
};
} else {
// jika tidak ditemukan
throw {
code: 404,
status: 'error',
message: 'Data marketing tidak ditemukan.',
};
}
}
// if (foundData) {
// // Data ditemukan, kembalikan respons sukses
// return createDummyResponse<Marketing>(foundData as Marketing);
// } else {
// // Data tidak ditemukan, simulasi respons error
// return {
// code: 404,
// status: 'error',
// message: 'Marketing data not found (MOCK)',
// };
// }
// }
/**
* Approve single marketing data
*/
async singleApproval(
id: number,
action: 'approve' | 'reject'
action: 'APPROVED' | 'REJECTED',
notes?: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
try {
const path = `${this.basePath}/approvals`;
@@ -88,8 +80,8 @@ export class MarketingService extends BaseApiService<
method: 'POST',
body: {
action: action,
approval_ids: [id],
notes: `${action} marketing ${id}`,
approvable_ids: [id],
notes: notes || `${action} marketing ${id}`,
},
});
} catch (error) {
@@ -103,7 +95,8 @@ export class MarketingService extends BaseApiService<
*/
async bulkApprovals(
ids: number[],
action: 'approve' | 'reject'
action: 'APPROVED' | 'REJECTED',
notes?: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
try {
const path = `${this.basePath}/approvals`;
@@ -111,8 +104,8 @@ export class MarketingService extends BaseApiService<
method: 'POST',
body: {
action: action,
approval_ids: ids,
notes: `${action} marketing ${ids.join(', ')}`,
approvable_ids: ids,
notes: notes || `${action} marketing ${ids.join(', ')}`,
},
});
} catch (error) {
@@ -120,6 +113,37 @@ export class MarketingService extends BaseApiService<
return undefined;
}
}
/**
* Delivery
*/
async delivery(
id: number,
notes?: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
try {
const path = `${this.basePath}/approvals`;
return await httpClient<BaseApiResponse<{ message: string }>>(path, {
method: 'POST',
body: {
action: 'APPROVED',
approvable_ids: [id],
notes: notes || `Delivery marketing ${id}`,
},
});
} catch (error) {
console.error('Error delivery marketing:', error);
return undefined;
}
}
}
export const MarketingApi = new MarketingService('/marketing');
export const SalesOrderApi = new SalesOrderService('/marketing/sales-orders');
export const DeliveryOrderApi = new BaseApiService<
Marketing,
CreateDeliveryOrderPayload,
UpdateDeliveryOrderPayload
>('/marketing/delivery-orders');
export const MarketingApi = new BaseApiService<Marketing, unknown, unknown>(
'/marketing'
);
+100 -3
View File
@@ -1,8 +1,17 @@
import { BaseApiService } from '@/services/api/base';
import { BaseApiService } from './base';
import { BaseApiResponse } from '@/types/api/api-general';
import {
CreateProjectFlockPayload,
ProjectFlock,
UpdateProjectFlockPayload,
} from '@/types/api/production/project-flock';
import {
CreateRecordingPayload,
Recording,
UpdateRecordingPayload,
CreateGradingPayload,
UpdateGradingPayload,
NextDayRecording,
} from '@/types/api/production/recording';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
@@ -11,8 +20,96 @@ export const ProjectFlockKandangApi = new BaseApiService<
unknown,
unknown
>('/production/project-flock-kandangs');
export const RecordingApi = new BaseApiService<
export const ProjectFlockApi = new BaseApiService<
ProjectFlock,
CreateProjectFlockPayload,
UpdateProjectFlockPayload
>('/production/project-flocks');
export class RecordingService extends BaseApiService<
Recording,
CreateRecordingPayload,
UpdateRecordingPayload
>('/flock/recordings');
> {
constructor(basePath: string = '') {
super(basePath);
}
async approve(
idOrIds: number | number[],
notes?: string
): Promise<BaseApiResponse<Recording[]> | undefined> {
const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
return await this.customRequest<BaseApiResponse<Recording[]>>('approvals', {
method: 'POST',
payload: {
action: 'APPROVED',
approvable_ids,
notes,
},
});
}
async reject(
idOrIds: number | number[],
notes: string = ''
): Promise<BaseApiResponse<Recording[]> | undefined> {
const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
return await this.customRequest<BaseApiResponse<Recording[]>>('approvals', {
method: 'POST',
payload: {
action: 'REJECTED',
approvable_ids,
notes,
},
});
}
async createGrading(
payload: CreateGradingPayload
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>('gradings', {
method: 'POST',
payload,
});
}
async updateGrading(
gradingId: number,
payload: UpdateGradingPayload
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>(
`gradings/${gradingId}`,
{
method: 'PUT',
payload,
}
);
}
async deleteGrading(
gradingId: number
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>(
`gradings/${gradingId}`,
{
method: 'DELETE',
}
);
}
async nextDayRecording(
projectFlockId: number
): Promise<BaseApiResponse<NextDayRecording> | undefined> {
return await this.customRequest<BaseApiResponse<NextDayRecording>>(
`next-day`,
{
method: 'GET',
params: {
project_flock_kandang_id: projectFlockId,
},
}
);
}
}
export const RecordingApi = new RecordingService('/production/recordings');
+3 -2
View File
@@ -21,7 +21,8 @@ export class ChickinService extends BaseApiService<
*/
async singleApproval(
id: number,
action: 'APPROVED' | 'REJECTED'
action: 'APPROVED' | 'REJECTED',
notes?: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
try {
const path = `${this.basePath}/approvals`;
@@ -30,7 +31,7 @@ export class ChickinService extends BaseApiService<
body: {
action: action,
approvable_ids: [id],
notes: `${action} chickin ${id}`,
notes: notes ?? `${action} chickin ${id}`,
},
});
} catch (error) {
@@ -0,0 +1,11 @@
import { BaseApiService } from '@/services/api/base';
import {
BaseProjectFlockKandang,
ProjectFlockKandang,
} from '@/types/api/production/project-flock-kandang';
export const ProjectFlockKandangApi = new BaseApiService<
BaseProjectFlockKandang,
ProjectFlockKandang,
unknown
>('project-flock-kandang');
+38 -27
View File
@@ -8,13 +8,10 @@ import {
BaseApiResponse,
BaseGroupedApproval,
ErrorApiResponse,
GroupedApprovals,
SuccessApiResponse,
} from '@/types/api/api-general';
import { sleep } from '@/lib/helper';
import { httpClient } from '@/services/http/client';
import axios from 'axios';
import { Flock } from '@/types/api/master-data/flock';
import { Kandang } from '@/types/api/master-data/kandang';
import { RequestOptions } from '@/services/http/base';
@@ -104,25 +101,34 @@ export class ProjectFlockService extends BaseApiService<
/**
* Get Next Period of Project Flock
*/
async getNextPeriod(id: string): Promise<
| BaseApiResponse<{
flock: Flock;
next_period: number;
}>
async getNextPeriod(locationId: number): Promise<
| BaseApiResponse<
{
id: number;
name: string;
period: number;
}[]
>
| ErrorApiResponse
| SuccessApiResponse<{
flock: Flock;
next_period: number;
}>
| SuccessApiResponse<
{
id: number;
name: string;
period: number;
}[]
>
| undefined
> {
try {
const path = `${this.basePath}/kandangs/${id}`;
const path = `${this.basePath}/location/${locationId.toString()}/periods`;
return await httpClient<
SuccessApiResponse<{
flock: Flock;
next_period: number;
}>
SuccessApiResponse<
{
id: number;
name: string;
period: number;
}[]
>
>(path, {
method: 'GET',
});
@@ -139,36 +145,40 @@ export class ProjectFlockService extends BaseApiService<
* Approve single Project Flock
*/
async approve(
id: number
id: number,
notes?: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
return await this.bulkApprovalAction([id], 'APPROVED');
return await this.bulkApprovalAction([id], 'APPROVED', notes);
}
/**
* Reject single Project Flock
*/
async reject(
id: number
id: number,
notes?: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
return await this.bulkApprovalAction([id], 'REJECTED');
return await this.bulkApprovalAction([id], 'REJECTED', notes);
}
/**
* Approve Bulk Project Flock
*/
async bulkApprove(
ids: number[]
ids: number[],
notes?: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
return await this.bulkApprovalAction(ids, 'APPROVED');
return await this.bulkApprovalAction(ids, 'APPROVED', notes);
}
/**
* Reject Bulk Project Flock
*/
async bulkReject(
ids: number[]
ids: number[],
notes?: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
return await this.bulkApprovalAction(ids, 'REJECTED');
return await this.bulkApprovalAction(ids, 'REJECTED', notes);
}
/**
@@ -176,7 +186,8 @@ export class ProjectFlockService extends BaseApiService<
*/
async bulkApprovalAction(
ids: number[],
action: 'APPROVED' | 'REJECTED'
action: 'APPROVED' | 'REJECTED',
notes?: string
): Promise<BaseApiResponse<{ message: string }> | undefined> {
try {
const path = `${this.basePath}/approvals`;
@@ -185,7 +196,7 @@ export class ProjectFlockService extends BaseApiService<
body: {
action: action,
approvable_ids: ids,
notes: `Bulk ${action} Project Flock ${ids.join(', ')}`,
notes: notes ?? `Bulk ${action} Project Flock ${ids.join(', ')}`,
},
});
} catch (error) {
-2
View File
@@ -71,5 +71,3 @@ export type CreateMovementPayload = {
}[];
}[];
};
export type UpdateMovementPayload = CreateMovementPayload;
+86 -18
View File
@@ -6,19 +6,55 @@ import {
} from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Kandang } from '@/types/api/master-data/kandang';
import { id } from 'react-day-picker/locale';
import { Warehouse } from '../master-data/warehouse';
/**
* Base Data Response
*/
export type BaseMarketing = {
id: number;
status?: string;
so_number: string;
customer: Customer;
so_docs: string;
so_date: string;
customer: Customer;
sales_person: CreatedUser;
notes: string;
grand_total: number;
approval: BaseApproval;
marketing_products?: MarketingProduct[];
latest_approval: BaseApproval;
sales_order: BaseSalesOrder[];
delivery_order: BaseDeliveryOrder[];
};
export type BaseSalesOrder = {
id: number;
marketing_id: number;
product_warehouse_id: number;
qty: number;
unit_price: number;
avg_weight: number;
total_weight: number;
total_price: number;
product_warehouse: ProductWarehouse;
vehicle_number: string;
};
export type BaseDeliveryOrder = {
id: number;
marketing_id: number;
do_number: string;
delivery_date: string;
warehouse: Warehouse;
deliveries: BaseDelivery[];
};
export type BaseDelivery = {
product_warehouse: ProductWarehouse;
qty: number;
unit_price: number;
total_weight: number;
avg_weight: number;
total_price: number;
vehicle_number: string;
};
export type MarketingProduct = {
@@ -29,7 +65,7 @@ export type MarketingProduct = {
total_weight: number;
total_price: number;
product_warehouse: ProductWarehouse;
marketing_delivery_products?: MarketingDeliveryProducts;
delivery_product?: MarketingDeliveryProducts;
};
export type MarketingDeliveryProducts = {
@@ -39,34 +75,66 @@ export type MarketingDeliveryProducts = {
avg_weight: number;
total_weight: number;
total_price: number;
delivery_date: string;
delivery_date: string | null;
vehicle_number: string;
do_number?: string | undefined;
do_number?: string | undefined; // Uncertain
};
export type Marketing = BaseMetadata & BaseMarketing;
export type CreateMarketingPayload = {
/**
* Base Data Payload
*/
export type BaseCreateMarketingPayload = {
customer_id: number;
sales_person_id: number;
date: string;
notes: string;
marketing_products: CreateMarketingProductPayload[];
};
export type UpdateMarketingPayload = CreateMarketingPayload;
export type CreateMarketingProductPayload = {
id?: number;
export type BaseCreateMarketingProductPayload = {
vehicle_number: string;
kandang_id: string | number | undefined;
kandang: Kandang | undefined;
product_warehouse_id: string | number | undefined;
product_warehouse: ProductWarehouse | undefined;
unit_price: string | number | undefined;
total_weight: string | number | undefined;
qty: string | number | undefined;
uom: string | undefined;
avg_weight: string | number | undefined;
total_price: string | number | undefined;
delivery_date?: string | null;
};
export type UpdateMarketingProductPayload = CreateMarketingProductPayload;
/**
* Payload Data Types Sales Order
*/
export type CreateSalesOrderPayload = BaseCreateMarketingPayload & {
marketing_products: CreateSalesOrderProductPayload[];
};
export type CreateSalesOrderProductPayload =
BaseCreateMarketingProductPayload & {
id?: number;
kandang?: Kandang | undefined;
product_warehouse?: ProductWarehouse | undefined;
};
export type CreateDeliveryOrderPayload = {
marketing_id?: number;
delivery_products: CreateDeliveryOrderProductPayload[];
};
export type CreateDeliveryOrderProductPayload =
BaseCreateMarketingProductPayload & {
id?: number;
marketing_product_id: number;
delivery_date: string;
};
export type UpdateSalesOrderProductPayload = CreateSalesOrderProductPayload;
export type UpdateDeliveryOrderProductPayload =
CreateDeliveryOrderProductPayload;
export type UpdateSalesOrderPayload = CreateSalesOrderPayload;
export type UpdateDeliveryOrderPayload = CreateDeliveryOrderPayload;
+18 -6
View File
@@ -7,12 +7,9 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
export type BaseProjectFlock = {
id: number;
name: string;
flock_name: string;
name?: string;
flock_name?: string;
status: string;
flock?: Flock;
flock_i?: number;
flock_name: string;
area: Area;
area_id: number;
category: string;
@@ -41,7 +38,6 @@ export type CreateProjectFlockPayload = {
category: string;
fcr_id: number;
location_id: number;
period: number;
kandang_ids: number[];
};
@@ -52,6 +48,16 @@ export type ProjectFlockApprovalPayload = {
approvable_ids: number[];
};
export type ProjectFlockKandangLookup = {
id: number;
project_flock_kandang_id: number;
project_flock_id: number;
kandang_id: number;
kandang: Kandang;
project_flock: ProjectFlock;
quantity: number;
};
export type ProjectFlockAvailableQuantity = {
project_flock_id: number;
flock_name: string;
@@ -61,3 +67,9 @@ export type ProjectFlockAvailableQuantity = {
available_qty: number;
}[];
};
export type ProjectFlockPeriods = {
id: number;
name: string;
period: number;
}[];
+134 -48
View File
@@ -1,61 +1,147 @@
import { BaseMetadata } from '@/types/api/api-general';
import { Location } from '@/types/api/master-data/location';
import { Kandang } from '@/types/api/master-data/kandang';
import { Flock } from '@/types/api/master-data/flock';
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
export type ProductionMetrics = {
total_depletion_qty: number;
cum_depletion_rate: number;
daily_gain: number;
avg_daily_gain: number;
cum_intake: number;
fcr_value: number;
total_chick_qty: number;
daily_depletion_rate?: number;
cum_depletion?: number;
};
export type BaseRecording = {
id: number;
flock: Flock;
recording_date: string;
location: Location;
coop: Kandang;
feed_data: {
feed_name: string;
feed_qty: number;
feed_stock: number;
}[];
body_weight: {
chicken_weight: number;
chicken_count: number;
average_chicken_weight: number;
}[];
vaccination: {
vaccine_name: string;
total_stock: number;
used_stock: number;
}[];
mortality: {
condition: string;
count: number;
project_flock_kandang_id: number;
record_datetime: string;
day: number;
created_by: User;
} & ProductionMetrics;
export type RecordingBW = {
id: number;
recording_id: number;
avg_weight: number;
qty: number;
total_weight: number;
};
export type RecordingDepletion = {
id: number;
recording_id: number;
product_warehouse_id: number;
qty: number;
product_warehouse: ProductWarehouse;
};
export type RecordingStock = {
id: number;
recording_id: number;
product_warehouse_id: number;
usage_amount?: number;
usage_qty: number;
qty: number;
pending_qty: number;
product_warehouse: ProductWarehouse;
};
export type RecordingEgg = {
id: number;
recording_id: number;
product_warehouse_id: number;
qty: number;
created_by: User;
product_warehouse: ProductWarehouse;
gradings?: {
grade: string;
qty: number;
}[];
};
export type Recording = BaseMetadata & BaseRecording;
export type GradingEgg = {
id: number;
recording_egg_id: number;
qty: number;
grade: string;
created_by: User;
};
export type CreateRecordingPayload = {
flock_id: number;
recording_date: string;
location_id: number;
coop_id: number;
feed_data: {
feed_id: string;
feed_qty: number;
feed_stock: number;
export type Recording = BaseMetadata &
BaseRecording & {
project_flock_category?: 'GROWING' | 'LAYING';
approval?: BaseApproval;
egg_grading_status?: string | null;
egg_grading_pending_qty?: number | null;
egg_grading_completed_qty?: number | null;
body_weights?: RecordingBW[];
depletions?: RecordingDepletion[];
stocks?: RecordingStock[];
eggs?: RecordingEgg[];
recording_bws?: RecordingBW[];
recording_depletions?: RecordingDepletion[];
recording_stocks?: RecordingStock[];
recording_eggs?: RecordingEgg[];
grading_eggs?: GradingEgg[];
};
export type NextDayRecording = {
project_flock_kandang_id: number;
next_day: number;
};
export type CreateGrowingRecordingPayload = {
project_flock_kandang_id: number;
body_weights: {
avg_weight: number;
qty: number;
}[];
body_weight: {
chicken_weight: number;
chicken_count: number;
average_chicken_weight: number;
stocks?: {
product_warehouse_id: number;
qty: number;
}[];
vaccination: {
vaccine_id: string;
total_stock: number;
used_stock: number;
}[];
mortality: {
condition: string;
count: number;
depletions?: {
product_warehouse_id: number;
qty: number;
}[];
};
export type CreateGradingPayload = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number;
}[];
};
export type UpdateGradingPayload = CreateGradingPayload;
export type CreateGradingRecordingPayload = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number;
}[];
};
export type CreateEggPayload = {
product_warehouse_id: number;
qty: number;
};
export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
eggs?: CreateEggPayload[];
};
export type CreateRecordingPayload =
| CreateGrowingRecordingPayload
| CreateLayingRecordingPayload
| CreateGradingRecordingPayload;
export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload;
export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload;
export type UpdateGradingRecordingPayload = CreateGradingRecordingPayload;
export type UpdateRecordingPayload = CreateRecordingPayload;