mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-21 05:45:46 +00:00
Compare commits
916 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77a7b6ccf8 | |||
| 0ac14fe342 | |||
| 00eed01cea | |||
| 6702dd7dc6 | |||
| a526772e02 | |||
| b486d25a8b | |||
| b73b452621 | |||
| 4f4787e088 | |||
| f32024d19a | |||
| 3b666a924f | |||
| 021df11600 | |||
| d306fad40c | |||
| ebc12638ff | |||
| 437dd75934 | |||
| e1c0701629 | |||
| 96a5eb1be5 | |||
| 7add41ea5a | |||
| 548dfd19e5 | |||
| f0a6dd4a5a | |||
| 7159114733 | |||
| e300a60b5a | |||
| 4cbd83a6fa | |||
| b39202111e | |||
| a891608d13 | |||
| aff05c6b1a | |||
| 70eac011f3 | |||
| 10e843aebf | |||
| a24f51dad3 | |||
| 9245285fe2 | |||
| eb1337292b | |||
| aa114c164b | |||
| 0f9849c0ac | |||
| 36b167dafb | |||
| 1002c6c437 | |||
| 25352659f3 | |||
| dfac7f84ff | |||
| c099314bdb | |||
| 404019f181 | |||
| 8afd8c6382 | |||
| ef4ad07547 | |||
| 31aa8e7652 | |||
| 994967e940 | |||
| 90eef08f9b | |||
| def2167803 | |||
| bf834cf79b | |||
| 470add1563 | |||
| 114658f198 | |||
| d01bfe33a8 | |||
| 47ba16b20a | |||
| d6c6211937 | |||
| 536b1c5b01 | |||
| f84fcb78b8 | |||
| aec5c89979 | |||
| f46a0610f5 | |||
| d879acc001 | |||
| 8516929056 | |||
| 595f2b5e9b | |||
| 6a4e8776bd | |||
| ec16c6c47e | |||
| 2b2dd0a026 | |||
| a8c12d0c92 | |||
| 334bd08e60 | |||
| ddd9a3d2da | |||
| 2b1d5290f3 | |||
| 935588b30e | |||
| 677025b4a2 | |||
| 0da9f9d651 | |||
| c752cad057 | |||
| cdfb59a70b | |||
| 91fcbffab6 | |||
| 33e3b86b82 | |||
| a012707bae | |||
| 00bc644ea9 | |||
| 126346dc52 | |||
| 777b06c690 | |||
| 1f96100390 | |||
| 7e5898a253 | |||
| 86651d3f3f | |||
| ebd3e14f0e | |||
| e9a1f4298f | |||
| cc8f258c41 | |||
| f60a07bc5b | |||
| 6dc93b1065 | |||
| 76c68d0d79 | |||
| f1ed22ff22 | |||
| d478ef5e22 | |||
| 6643fe5a60 | |||
| 7a6b003cb9 | |||
| e6cee4a93a | |||
| c61ef5471b | |||
| c9c618e3f8 | |||
| ceb7cb6b2d | |||
| f765895988 | |||
| b1715172db | |||
| d84b2583d8 | |||
| f38cebc0d9 | |||
| 7951754722 | |||
| 4fc689898f | |||
| 69d7f65b76 | |||
| b626c2805f | |||
| 97c16ce596 | |||
| 88e3ec7bbc | |||
| 3ce30115f8 | |||
| 4fdfe63dc9 | |||
| badb1e141a | |||
| c894f26d18 | |||
| 3b9599d169 | |||
| 9a9a9c0cb5 | |||
| b69126ed84 | |||
| 91b0bf7c27 | |||
| 1b2a45f9ac | |||
| cb62416552 | |||
| d2781b0a89 | |||
| 325f3f1bd8 | |||
| 346c731c42 | |||
| 61766d3255 | |||
| 0898892f15 | |||
| 3dd4a9cebc | |||
| 549e15499c | |||
| a4a07f2ce9 | |||
| 6930696692 | |||
| 7df9559f35 | |||
| eeeb0a404c | |||
| b3c4a438ad | |||
| a9e6f7155e | |||
| 3d94474d7c | |||
| 4872a53a25 | |||
| 662dec38bc | |||
| 9f0cefe91c | |||
| 3cb6bfcf52 | |||
| 7d4869fbdc | |||
| 4ae36ee3f0 | |||
| 21acb09f0c | |||
| f17bc1493b | |||
| 6943cd3903 | |||
| 14b63dd0f1 | |||
| db4d9ad38c | |||
| 06dd9a3609 | |||
| 5ba58c92d4 | |||
| d44de5a363 | |||
| b70ae164e1 | |||
| f89236bb5e | |||
| 4759a82034 | |||
| 866bd90b3d | |||
| b0dfc7f31c | |||
| 561a9fe186 | |||
| dbdfd2c50b | |||
| afc102e618 | |||
| 7736fce5bb | |||
| 04dcf110a3 | |||
| 53bca6170f | |||
| 20e27dccc1 | |||
| 965dc01e86 | |||
| 64a0a9c8a8 | |||
| 0ed30e184b | |||
| 13205ca80a | |||
| 0d6e229ee5 | |||
| 319afa3faf | |||
| 6da6cf699f | |||
| ca830f8e3d | |||
| 22be41058f | |||
| c4debcecce | |||
| 6fdff6706d | |||
| d5b4111ae4 | |||
| 2df86e7be8 | |||
| 6efe44ba55 | |||
| c766f53753 | |||
| 38dfeec892 | |||
| cf8ed9ccad | |||
| 90c61cbdf6 | |||
| bfcdb9883d | |||
| b0a1b837d0 | |||
| 09ae619829 | |||
| 59f4528841 | |||
| d049f6c34f | |||
| 8b7ed9e46b | |||
| e6172be81e | |||
| 324b9b14ef | |||
| a10c20394d | |||
| ff2d53a0b7 | |||
| 42d8896ab7 | |||
| 50d499005d | |||
| b421bc48d0 | |||
| f7986149e8 | |||
| 309a9ecc86 | |||
| d30979f5cd | |||
| 43cd3abe02 | |||
| 770f363c60 | |||
| 88c6c863e7 | |||
| ba84e718cb | |||
| fed2bf7878 | |||
| 6f90bd604a | |||
| fa199e4879 | |||
| 46c06ea548 | |||
| fd868eaa0c | |||
| 8dfccf25d8 | |||
| 5d8dfca3b5 | |||
| aee0ad8a20 | |||
| 2fa086bb32 | |||
| 0af612703a | |||
| f22c4e4798 | |||
| 841aadc107 | |||
| f31a80340b | |||
| 727fd2ad43 | |||
| a4275f4b66 | |||
| 9ac5e0ee76 | |||
| 24499d110a | |||
| c24aebe02d | |||
| fd32b55ad9 | |||
| d2b19cbd7b | |||
| 2c29cffa45 | |||
| 476cf2fa29 | |||
| 1797498df2 | |||
| f7d1beffcf | |||
| ac6e7c6d36 | |||
| d7ef86e24b | |||
| 3dff72b0f0 | |||
| eea76e77a8 | |||
| 11f9a685a8 | |||
| c863ebc2af | |||
| e8aea0a27e | |||
| 79cf777b11 | |||
| 203782c258 | |||
| 0a0a1a23f9 | |||
| f9f4c5b67c | |||
| 98107373e8 | |||
| 9838cf347b | |||
| f183ee5c7a | |||
| 2ab2cd6d99 | |||
| 1571d79685 | |||
| 5e3648b385 | |||
| 62b05bf9c0 | |||
| 7579cd5533 | |||
| 174e5ed1a3 | |||
| c6e9e98ce1 | |||
| 9562ce7b40 | |||
| 57fa67c05a | |||
| eadb08879d | |||
| 50b8b77f22 | |||
| 045913d05f | |||
| b53c8b99a0 | |||
| f23d369e02 | |||
| d6b8b10183 | |||
| 1c77deeee7 | |||
| 6a3d2c0dcd | |||
| b1f4b4dc4b | |||
| a518895096 | |||
| 046fb74cab | |||
| 00f09364b1 | |||
| 02c5cddc94 | |||
| 9f079c1e52 | |||
| c32074d72c | |||
| 7d46e3dc2e | |||
| e7b53efa4b | |||
| aebb9a0b28 | |||
| 4ec62c936e | |||
| f7ae1f835f | |||
| 3769309ce3 | |||
| 15b3151c5f | |||
| b0707db551 | |||
| 1a8d794a66 | |||
| 1b7e8a342f | |||
| f08fae4f77 | |||
| 6cd09a413d | |||
| 84eb34a9dd | |||
| 37317ed95c | |||
| 201c9249cc | |||
| 46dfacae23 | |||
| d856b35e24 | |||
| 28c94e3e1d | |||
| a1e8f582ba | |||
| f3f552bd16 | |||
| 5af00faa32 | |||
| f0dcb6b8ca | |||
| e32b9ddcb2 | |||
| 57ef4109f7 | |||
| ef3611e7fc | |||
| 0090961ec0 | |||
| 640cf26970 | |||
| d1d0692e2e | |||
| 6b13794ee5 | |||
| 8e88355894 | |||
| 1b98e5d4d8 | |||
| d8daf09844 | |||
| 0bb9aee139 | |||
| 306b8d3bf3 | |||
| 2bf764a05c | |||
| 6c3285f624 | |||
| afb79b0589 | |||
| 4f571f1c16 | |||
| 81ca60a09b | |||
| 23453eb8f5 | |||
| 3dc32da834 | |||
| f089492830 | |||
| 3412994d15 | |||
| 6eaa92dfd4 | |||
| 8d668429e1 | |||
| 8d1a3b665e | |||
| 7b99bae197 | |||
| dad169d854 | |||
| f985c041e4 | |||
| 5326eff293 | |||
| d66bd8c606 | |||
| ea5ad20684 | |||
| 23ee8828f0 | |||
| 0dd2edfe01 | |||
| 6edc278bdf | |||
| f81c49becb | |||
| 03a9451fc8 | |||
| cc0b051a0a | |||
| 865438e3fb | |||
| d39b71e759 | |||
| 5e6b03ef08 | |||
| c291ba3246 | |||
| ab2e7db9d0 | |||
| 13c1a82142 | |||
| 6185fafb57 | |||
| 2ab7c10d5d | |||
| bc6ebcfeda | |||
| 10fb9fc990 | |||
| 28639516d5 | |||
| 2bf0f2874e | |||
| a81a61135f | |||
| d2e88c2061 | |||
| 8f4f3d93b8 | |||
| 7daca04cc1 | |||
| 2c5168badf | |||
| 73f4b486c0 | |||
| ed7ee1a268 | |||
| 4e7b91a7b4 | |||
| 7c0581728e | |||
| 52cb440cb3 | |||
| 7c64870fed | |||
| 7290f242f4 | |||
| 9be09ae281 | |||
| 32088b916f | |||
| f51236fcfc | |||
| c385c42c8f | |||
| 02dc624036 | |||
| 4e5f9c710c | |||
| 2712821f4e | |||
| d6849a48d2 | |||
| 2e44371c6c | |||
| 98ae56a1aa | |||
| 7fcab4d295 | |||
| 550bcc426b | |||
| 844ac01b70 | |||
| 9ef232bac5 | |||
| 34ec650a01 | |||
| 1d27781c02 | |||
| e81c0a3baf | |||
| be0bdcd299 | |||
| 7e73c99074 | |||
| 0f86ba0f2d | |||
| 10a37fde75 | |||
| 6ad1a3349b | |||
| 3bb5d5e5a5 | |||
| 3279fb30ce | |||
| 34eae71b44 | |||
| 4f168b51c7 | |||
| ded1cc1f62 | |||
| 39f2fc48a8 | |||
| a72b22da6e | |||
| dc4e569453 | |||
| 17a6cee1e3 | |||
| 5e32724d40 | |||
| cd3a5ad441 | |||
| 11bd8b27b5 | |||
| 9a1a6a7e41 | |||
| d02f919b76 | |||
| 4529ee50e3 | |||
| cd42bd6bc0 | |||
| 4ed1e4f8b5 | |||
| ea7f8a68f4 | |||
| 11a63f76b7 | |||
| cd41d5daab | |||
| 9f2fcbf154 | |||
| 70d9b4d8ed | |||
| 39f70bd71b | |||
| 817f8a7010 | |||
| 2276df2790 | |||
| 8ec76af012 | |||
| 9f0dc8c644 | |||
| 2d0c8dbd3f | |||
| 8224dbf8ec | |||
| 6e4462e217 | |||
| b1ccad081d | |||
| c0a818af7e | |||
| e0371b0884 | |||
| 8a6f78ef84 | |||
| b2c09bb7c7 | |||
| c550922974 | |||
| 432d837aaf | |||
| b24fb54856 | |||
| f37eea687a | |||
| 77b05c6440 | |||
| 731bec5a94 | |||
| 6ea25aa3b1 | |||
| d4f4505405 | |||
| bd653851e2 | |||
| c8f47c741a | |||
| 78486be3ea | |||
| fe04bf5692 | |||
| 3c29b8bc77 | |||
| 45d65024db | |||
| fd2077c68b | |||
| 819b709f7e | |||
| 549a710a8d | |||
| ec8ae7561d | |||
| 5f68c05acc | |||
| b9b349aa7a | |||
| 0a447f93c1 | |||
| 6ec6323bbc | |||
| c44e63bd2b | |||
| 500c30c2bc | |||
| 507c4005af | |||
| d49bca1d40 | |||
| e4d75dad68 | |||
| 4f22024c82 | |||
| 751c27b73e | |||
| 0d77aa4a5f | |||
| 6fde6b180a | |||
| 663c1dea14 | |||
| 4aab54981e | |||
| 04c987b86b | |||
| 800739bd4f | |||
| e0ee846106 | |||
| 84b49d2ac6 | |||
| ec95ddbddd | |||
| 6d2057842d | |||
| 1843a47d59 | |||
| 9e0d3e2bbf | |||
| e6a38c3f65 | |||
| f58cb43801 | |||
| 517e8c758c | |||
| 97c5917401 | |||
| 4be719b9d8 | |||
| 31a9828661 | |||
| 580c357667 | |||
| 1152cd2bef | |||
| f1227c9dcb | |||
| 5f3c3be1f3 | |||
| ae00f49e64 | |||
| d9322cc17d | |||
| f5f154883b | |||
| 8c21883aa9 | |||
| 4ddd1dc8e3 | |||
| 879702d31d | |||
| 8c95dc8327 | |||
| f0eb3fcf52 | |||
| df6c1ae49d | |||
| 42a56a08d7 | |||
| 871f0403ad | |||
| b57a0fcc90 | |||
| 6ef9c1338f | |||
| ea4dd29bbf | |||
| d591c89cac | |||
| 22d24af41c | |||
| 6ed7dcfa6d | |||
| dda29e10d1 | |||
| 20d124504b | |||
| b9c1989cae | |||
| 5fae7752f2 | |||
| 9d9b9d93db | |||
| f41899dbc9 | |||
| 9e5d878e82 | |||
| 035f187bac | |||
| cb78ec4990 | |||
| 3a2fac013e | |||
| 3b2e11fd41 | |||
| 414d617341 | |||
| 0774200aa5 | |||
| 5dd64b9907 | |||
| 8fc5d42bb8 | |||
| 36ff6d04ee | |||
| f23a0144b0 | |||
| de63b6721a | |||
| a200dac23c | |||
| 09dd907f88 | |||
| 33b8d0a8b0 | |||
| 398282b3bf | |||
| 035482accc | |||
| fcfd2fb576 | |||
| 2c28d0a831 | |||
| 907afbb062 | |||
| addfaff692 | |||
| ecdbb764d5 | |||
| a3be3de338 | |||
| 9e895af62a | |||
| 1f9992c1c8 | |||
| 574fb3b371 | |||
| 4643a39c3e | |||
| 88b8767ca4 | |||
| de19cc5de2 | |||
| b9dad3094c | |||
| a4b9b3fd2f | |||
| b91a199d13 | |||
| ff427d13cc | |||
| 8295943b82 | |||
| a3169d582d | |||
| bf16d259bd | |||
| 5ae299a4b5 | |||
| c840f881bb | |||
| 6f16cf6deb | |||
| 5b4bc136f2 | |||
| dd6c6263e7 | |||
| 346d655406 | |||
| 5ff132070c | |||
| 398a09bf3b | |||
| 4e1315a027 | |||
| dcf2acc799 | |||
| 67a3ce2906 | |||
| 1d28e80b66 | |||
| 9dae6f1e95 | |||
| 5d03b68576 | |||
| 4bc9926356 | |||
| ea32056ca8 | |||
| ddfdfe4d91 | |||
| 7ea16d6a8a | |||
| 206d6c0b4e | |||
| 382721059a | |||
| 6a71828167 | |||
| a5e79570c5 | |||
| 804aa700d3 | |||
| 982a5d0d11 | |||
| 478e9eb541 | |||
| 9e0631a415 | |||
| 18ca7d8a59 | |||
| eb8a1567c6 | |||
| a0e63ea2d4 | |||
| 1ac35691ff | |||
| f9aa254c18 | |||
| c8effe4473 | |||
| c230c8000b | |||
| a7267370a0 | |||
| daddebc0a6 | |||
| 856d1f5c0c | |||
| da5a577fde | |||
| c36d1ee153 | |||
| 7259de8b14 | |||
| 9e576cf444 | |||
| d7b828cb47 | |||
| f757e5f6ba | |||
| 7f694c7298 | |||
| 5326fc918a | |||
| 6658312427 | |||
| de73c626b1 | |||
| faaa10b74b | |||
| d66eaf08c0 | |||
| a6a6ff9f72 | |||
| 5a21a3b44c | |||
| 00e0126e42 | |||
| 2f23755510 | |||
| 996e132660 | |||
| e3eda4f5e4 | |||
| 7cc616ff41 | |||
| 0b75d68494 | |||
| 83224e046b | |||
| 096a8d394e | |||
| 11bf6ad760 | |||
| c8a834f84a | |||
| ead5ba759d | |||
| 8ceca2cc59 | |||
| 8dc23f83cd | |||
| 57f53b0a04 | |||
| 7e0aa4f790 | |||
| 2fe4ec981c | |||
| cf41fbfdaf | |||
| 86cef78a12 | |||
| fa63bd8ff9 | |||
| d9b41a6760 | |||
| c9cf33f1ad | |||
| 33d8d2aa2a | |||
| 61d85154fd | |||
| 466ea47121 | |||
| 3a35c72e06 | |||
| 09d36f504b | |||
| b9b7e45bc7 | |||
| e49c247f02 | |||
| b8c6f94db8 | |||
| 5def3c9f17 | |||
| 447b8067f7 | |||
| 4a8f2b1e1d | |||
| 36389bae2a | |||
| d001b05c4e | |||
| 20494657c6 | |||
| 2d8e479b6c | |||
| fef7f0e29b | |||
| 81d242bd1d | |||
| 1be596921a | |||
| 8fb1ccbdce | |||
| 85fddcb19a | |||
| 915e68f755 | |||
| 87adbf8547 | |||
| e8492f87ba | |||
| ceae338c73 | |||
| fa7824224c | |||
| 6b30457ec2 | |||
| 843fa6ee7a | |||
| a935ffd9f5 | |||
| 1de98db4ba | |||
| f844c9ff2c | |||
| 83fc92d48b | |||
| 4edd4f1285 | |||
| cc3765abcd | |||
| 3497a6346c | |||
| 69b4ca455e | |||
| 320bc52244 | |||
| 40f2d0ba93 | |||
| 481a643b3c | |||
| 9b2d98f7ce | |||
| 3e8c29df64 | |||
| 6155929e14 | |||
| 7c9f68d3a3 | |||
| 918e85e0cc | |||
| bb80e9e9c6 | |||
| 80fd75dfc1 | |||
| e1b562c175 | |||
| 9365320b03 | |||
| e515438312 | |||
| 530ef4982d | |||
| a8c3b1a66f | |||
| 62d4d7b7db | |||
| 57df2e9aed | |||
| 9ea86fe5c3 | |||
| ecf1677c27 | |||
| c1e075b1ff | |||
| c70cfbd450 | |||
| 4cc41c0167 | |||
| 730b7903a7 | |||
| 78efd587be | |||
| 3d91c12874 | |||
| 12e6d60745 | |||
| 01313f0b09 | |||
| 9f521a6a08 | |||
| 43afc5781c | |||
| 0f7f4e891c | |||
| b02b458034 | |||
| 304084ea2c | |||
| 204a1098a3 | |||
| c04cd29ac7 | |||
| 224f7ddeea | |||
| d17c11e2f2 | |||
| f58b03ba0e | |||
| c9544e1bd0 | |||
| d348cee4e6 | |||
| 1560908101 | |||
| 874bc27e4c | |||
| be238779a4 | |||
| aea39a878a | |||
| 84e562e22c | |||
| 8db7b6d5e9 | |||
| 5c8bc4fc6e | |||
| 9ba3fa1b6c | |||
| 6f18c58042 | |||
| 0d8e642b4e | |||
| 615d4d5ffe | |||
| 2a00da0298 | |||
| 68437b3b7e | |||
| 31b2a5a548 | |||
| 4d997256ad | |||
| 7ea9e10ad2 | |||
| 245da2595c | |||
| 45f1e923b7 | |||
| ebe752b27b | |||
| 3c3c2345c7 | |||
| 3f78cfdb63 | |||
| 69eaae6d43 | |||
| 65b60cc464 | |||
| 5c9332537c | |||
| 63c2a240d2 | |||
| fd2e1f8b96 | |||
| fdb3e0481a | |||
| 9bc632c286 | |||
| 81f98c5f06 | |||
| 0983f154d2 | |||
| bd4c51cb04 | |||
| 9c09395677 | |||
| 67b5187d39 | |||
| 5de5dcffc0 | |||
| 51ff739926 | |||
| 238edfeca2 | |||
| bd8d121113 | |||
| 38b91a57f0 | |||
| d0abc0e9ff | |||
| 4262e8e286 | |||
| 48c163c1cd | |||
| 3c03494bd3 | |||
| 3dd36b8248 | |||
| 12698004e1 | |||
| a0ca8e8f69 | |||
| 69206d4524 | |||
| a73f9a1acd | |||
| 4ea56f2e18 | |||
| 48649df409 | |||
| c53f9352be | |||
| df632526d2 | |||
| 4ec455b3b7 | |||
| 4f4fd3e6b7 | |||
| 0d7dd0a110 | |||
| 9bf4fd585d | |||
| a77a360410 | |||
| 9628ee88ad | |||
| 4356bd8803 | |||
| cbf1660da5 | |||
| 37f59f9470 | |||
| 6e3b25eb98 | |||
| f939f4b0fb | |||
| 720ff2128f | |||
| 280fffe6a5 | |||
| 6340a5e519 | |||
| d4fc0b4a4f | |||
| 4f595c7cad | |||
| 3826b8ea53 | |||
| 5cc82f1615 | |||
| cfaac14820 | |||
| b39d1f5c2e | |||
| 30ab48e426 | |||
| 88c640df18 | |||
| 017b081832 | |||
| 83d76f7de4 | |||
| 9af140e58d | |||
| 654aa50cc7 | |||
| 814e8db1ba | |||
| d1883654bc | |||
| 2c6ad71fd3 | |||
| 6c31d933b0 | |||
| b806c0f0a1 | |||
| a073488c2c | |||
| 7efb2a4dbb | |||
| 01d1ed8f0d | |||
| aed58ef10c | |||
| f105852a07 | |||
| 21d6fc8579 | |||
| eea1fcb513 | |||
| 312580a7fc | |||
| 7c4d5e68fa | |||
| b74e43c483 | |||
| dbff1bda3d | |||
| 244c564f06 | |||
| 757e0435ac | |||
| 46d70e36dd | |||
| 0cc9d0e94e | |||
| d7199fad53 | |||
| 8c2683c440 | |||
| 9c7033b53a | |||
| 07bfe0a20b | |||
| b9e69b243f | |||
| 8dec4915a2 | |||
| 7be32326a9 | |||
| c416fdbdaa | |||
| 270e8ff0c6 | |||
| 64abc5001d | |||
| f48cfca650 | |||
| a116f7ca66 | |||
| 429f5ffb62 | |||
| eed142a85f | |||
| 48f228de1c | |||
| c92abfc9ab | |||
| 7e999b2e34 | |||
| e90c7d993c | |||
| 99fbcaaea3 | |||
| 865b0b3d8f | |||
| a4c83f99a7 | |||
| e23b698fc7 | |||
| 294c971fea | |||
| 8a8128a692 | |||
| 649dd70ea7 | |||
| 44b9210ccc | |||
| e3f90a49d0 | |||
| a1679ba5ff | |||
| 8a7149c123 | |||
| f87854ed07 | |||
| 1ef7130661 | |||
| 066c356d4f | |||
| 8c7640eb9c | |||
| 8f5dd1851a | |||
| 489815ecaf | |||
| f9dfe7b27f | |||
| 6a926f881d | |||
| 68dd5b1121 | |||
| 5a3c7d71b0 | |||
| 5ad61d483a | |||
| 1567a4016f | |||
| 8b8702b1b8 | |||
| b039ec832b | |||
| abf2735b86 | |||
| a26099b507 | |||
| d7384752a0 | |||
| dfecef2e0c | |||
| 80fd8bb7ba | |||
| 44b9f94cec | |||
| e3b3f5ccdc | |||
| 33691f45bb | |||
| 2c72c44be4 | |||
| 98dfd4564c | |||
| a795d78c80 | |||
| 8a0adf847e | |||
| 8e80d668fa | |||
| a45de4fb13 | |||
| 6ee5bc3f1b | |||
| 012fe800bc | |||
| c3835d5128 | |||
| 7c4bd81364 | |||
| 545af8267a | |||
| 2e6a724b2f | |||
| 305b8e5005 | |||
| 5deca5739f | |||
| b464432581 | |||
| 512ad5175e | |||
| a7d884b5f0 | |||
| ce75eb25d7 | |||
| c7911f01f2 | |||
| 68874a1c14 | |||
| 7cc2a31745 | |||
| f5663b82aa | |||
| 3a7f1f4812 | |||
| 32ffc1f14c | |||
| 58fb9b0c08 | |||
| 3569955e7f | |||
| 7df743ebf5 | |||
| 86a0faaa52 | |||
| df3f342214 | |||
| c3c1bbbe96 | |||
| dc0fd7a3ed | |||
| 5782abb531 | |||
| 2d1cabb86b | |||
| b362fd1748 | |||
| 3411aa9b1b | |||
| 1f29e3cb50 | |||
| b671de1336 | |||
| 090a3183f7 | |||
| 17865d733d | |||
| 5be67ef01c | |||
| 7f326bedd4 | |||
| c350bc0be2 | |||
| 6f7627ac92 | |||
| 1ae5c1bd64 | |||
| 5bb366026d | |||
| 9888dc4356 | |||
| 7615daa22a | |||
| 435cc0aedc | |||
| d189252551 | |||
| d85cf29193 | |||
| 84ff5e178b | |||
| 72840e2193 | |||
| ea2ada8224 | |||
| b97cc39854 | |||
| 195bbbe449 | |||
| 375b50b646 | |||
| a5c71ff8ce | |||
| e09074eed0 | |||
| ffbf886718 | |||
| b3f7b8a3c5 | |||
| e407410c4a | |||
| 341cb42452 | |||
| 99b9df27a7 | |||
| 27c867036f | |||
| c9552dec2d | |||
| aad24c3c58 | |||
| ff1493b520 | |||
| 4ff1649991 | |||
| 4fe53f364a | |||
| 85fdb4f7dd | |||
| 885e4250fd | |||
| eaf118845c | |||
| 30db7ee95d | |||
| 5869e0434b | |||
| f205c66509 | |||
| 46e072bbcf | |||
| c31b284cf4 | |||
| bac3f30ce3 | |||
| be725d42c3 | |||
| b37c3f87b0 | |||
| ae4c17b391 | |||
| 48dd6d7218 | |||
| cee3d4ba90 | |||
| a8d7fdc30d | |||
| 2bb2da74e6 | |||
| fd024fdb8f | |||
| 79a89ea193 | |||
| 611655e408 | |||
| 702943c55c | |||
| 075d945a59 | |||
| 7d9a88cf3b | |||
| b095208fae | |||
| c69d9dd605 | |||
| a1d0c7b331 | |||
| e0a8514814 | |||
| 949761d59d | |||
| 15ced14e20 | |||
| 492efb18e2 | |||
| 8ea29579ec | |||
| dc6b0eaec6 | |||
| 1a4a05308f | |||
| ba40bbb1d3 | |||
| 647b002065 | |||
| 991a594ee1 | |||
| 3b846bf11c | |||
| 3e07316678 | |||
| 411c2586f5 | |||
| 3a87b039bf | |||
| 50559caf52 | |||
| 8fbe6aa148 | |||
| 873a4b308d | |||
| f0ec758d7f | |||
| 88878f7613 | |||
| 31f758d680 | |||
| 9eba5ffeca | |||
| 6b5838b5aa | |||
| c76f3a3715 | |||
| 48435a9cbb | |||
| 2ace95a0db | |||
| 892bb19dfd | |||
| 7a76719547 | |||
| 4b6144d0b4 |
@@ -42,3 +42,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
# idea
|
# idea
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# claude
|
||||||
|
.claude
|
||||||
|
|||||||
+48
-29
@@ -2,6 +2,17 @@ stages:
|
|||||||
- build
|
- build
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# ✅ Global defaults
|
||||||
|
# ==========================================================
|
||||||
|
default:
|
||||||
|
tags:
|
||||||
|
- server-development-biznet
|
||||||
|
interruptible: true
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# 🏗️ Build Template
|
||||||
|
# ==========================================================
|
||||||
.build_template: &build_template
|
.build_template: &build_template
|
||||||
stage: build
|
stage: build
|
||||||
image: node:20-alpine
|
image: node:20-alpine
|
||||||
@@ -39,6 +50,9 @@ stages:
|
|||||||
- out/
|
- out/
|
||||||
expire_in: 1 week
|
expire_in: 1 week
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# 🚀 Deploy Template
|
||||||
|
# ==========================================================
|
||||||
.deploy_template: &deploy_template
|
.deploy_template: &deploy_template
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image:
|
image:
|
||||||
@@ -73,8 +87,8 @@ stages:
|
|||||||
|
|
||||||
if [ "$CI_COMMIT_BRANCH" = "development" ]; then
|
if [ "$CI_COMMIT_BRANCH" = "development" ]; then
|
||||||
ENVIRONMENT_NAME="WEB-LTI-DEV"
|
ENVIRONMENT_NAME="WEB-LTI-DEV"
|
||||||
elif [ "$CI_COMMIT_BRANCH" = "master" ]; then
|
elif [ "$CI_COMMIT_BRANCH" = "staging" ]; then
|
||||||
ENVIRONMENT_NAME="WEB-LTI-PROD"
|
ENVIRONMENT_NAME="WEB-LTI-STAGING"
|
||||||
else
|
else
|
||||||
ENVIRONMENT_NAME="UNKNOWN"
|
ENVIRONMENT_NAME="UNKNOWN"
|
||||||
fi
|
fi
|
||||||
@@ -82,11 +96,11 @@ stages:
|
|||||||
if [ "$STATUS" = "success" ]; then
|
if [ "$STATUS" = "success" ]; then
|
||||||
COLOR=3066993
|
COLOR=3066993
|
||||||
TITLE="✅ Deployment ${ENVIRONMENT_NAME} Succeeded"
|
TITLE="✅ Deployment ${ENVIRONMENT_NAME} Succeeded"
|
||||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully."
|
DESC="Deployment job on branch \${CI_COMMIT_REF_NAME}\ completed successfully."
|
||||||
else
|
else
|
||||||
COLOR=15158332
|
COLOR=15158332
|
||||||
TITLE="❌ Deployment ${ENVIRONMENT_NAME} Failed"
|
TITLE="❌ Deployment ${ENVIRONMENT_NAME} Failed"
|
||||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` encountered issues."
|
DESC="Deployment job on branch \${CI_COMMIT_REF_NAME}\ encountered issues."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
jq -n \
|
jq -n \
|
||||||
@@ -114,7 +128,9 @@ stages:
|
|||||||
|
|
||||||
curl -sS -H "Content-Type: application/json" -d @payload.json "$DISCORD_WEBHOOK_URL"
|
curl -sS -H "Content-Type: application/json" -d @payload.json "$DISCORD_WEBHOOK_URL"
|
||||||
|
|
||||||
# ====== DEVELOPMENT (Branch development) ======
|
# ==========================================================
|
||||||
|
# ==== DEVELOPMENT (Branch development) ======
|
||||||
|
# ==========================================================
|
||||||
build:dev:
|
build:dev:
|
||||||
<<: *build_template
|
<<: *build_template
|
||||||
rules:
|
rules:
|
||||||
@@ -122,11 +138,10 @@ build:dev:
|
|||||||
environment:
|
environment:
|
||||||
name: development
|
name: development
|
||||||
variables:
|
variables:
|
||||||
# NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
|
|
||||||
# NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
|
|
||||||
NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id'
|
NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
||||||
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
|
|
||||||
deploy:dev:
|
deploy:dev:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
@@ -141,26 +156,30 @@ deploy:dev:
|
|||||||
name: development
|
name: development
|
||||||
url: https://dev-lti-erp.mbugroup.id
|
url: https://dev-lti-erp.mbugroup.id
|
||||||
|
|
||||||
# ====== PRODUCTION ======
|
# ==========================================================
|
||||||
# build:production:
|
# ====== STAGING (Branch staging) ======
|
||||||
# <<: *build_template
|
# ==========================================================
|
||||||
# rules:
|
build:staging:
|
||||||
# # pilih salah satu: pakai branch master ATAU pakai tags rilis
|
<<: *build_template
|
||||||
# - if: '$CI_COMMIT_BRANCH == "master"'
|
rules:
|
||||||
# # - if: '$CI_COMMIT_TAG' # kalau mau rilis via tag, uncomment ini dan hapus baris di atas
|
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||||
# environment:
|
environment:
|
||||||
# name: production
|
name: staging
|
||||||
|
variables:
|
||||||
# deploy:production:
|
NEXT_PUBLIC_LTI_URL: 'https://stg-lti-erp.mbugroup.id'
|
||||||
# <<: *deploy_template
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
|
||||||
# needs: ["build:production"]
|
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
|
||||||
# rules:
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
# - if: '$CI_COMMIT_BRANCH == "master"'
|
|
||||||
# # - if: '$CI_COMMIT_TAG' # selaras dengan rule di build:production
|
|
||||||
# variables:
|
|
||||||
# S3_BUCKET: "lti-erp.mbugroup.id"
|
|
||||||
# CLOUDFRONT_DISTRIBUTION_ID: "ddfd"
|
|
||||||
# environment:
|
|
||||||
# name: production
|
|
||||||
|
|
||||||
|
|
||||||
|
deploy:staging:
|
||||||
|
<<: *deploy_template
|
||||||
|
needs: ['build:staging']
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||||
|
variables:
|
||||||
|
S3_BUCKET: 'stg-lti-erp.mbugroup.id'
|
||||||
|
AWS_REGION: 'ap-southeast-3'
|
||||||
|
CLOUDFRONT_DISTRIBUTION_ID: 'E2V6PPO1AUIU7H'
|
||||||
|
environment:
|
||||||
|
name: staging
|
||||||
|
url: https://stg-lti-erp.mbugroup.id
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run build
|
npx tsc --noEmit
|
||||||
@@ -3,6 +3,7 @@ import type { NextConfig } from 'next';
|
|||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: 'export',
|
output: 'export',
|
||||||
images: { unoptimized: true },
|
images: { unoptimized: true },
|
||||||
|
trailingSlash: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Generated
+3466
-34
File diff suppressed because it is too large
Load Diff
+20
-5
@@ -15,20 +15,35 @@
|
|||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
|
"jspdf": "^3.0.4",
|
||||||
|
"jspdf-autotable": "^5.0.2",
|
||||||
|
"lucide-react": "^0.562.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"next": "15.5.7",
|
"next": "15.5.9",
|
||||||
"react": "19.1.0",
|
"next-themes": "^0.4.6",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
|
"react": "^19.1.2",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "^19.1.2",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
|
"react-hook-form": "^7.70.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-number-format": "^5.4.4",
|
"react-number-format": "^5.4.4",
|
||||||
|
"react-resizable-panels": "2.1.7",
|
||||||
"react-select": "^5.10.2",
|
"react-select": "^5.10.2",
|
||||||
|
"recharts": "^3.6.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"swr": "^2.3.6",
|
"swr": "^2.3.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"use-debounce": "^10.0.6",
|
"use-debounce": "^10.0.6",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"yup": "^1.7.0",
|
"yup": "^1.7.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
@@ -39,9 +54,9 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"daisyui": "^5.1.12",
|
"daisyui": "^5.5.8",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.3",
|
"eslint-config-next": "^15.5.7",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import ClosingDetail from '@/components/pages/closing/ClosingDetail';
|
||||||
|
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
|
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||||
|
|
||||||
|
const ClosingDetailPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const closingId = searchParams.get('closingId');
|
||||||
|
const kandangId = searchParams.get('kandangId'); // project flock kandang ID
|
||||||
|
|
||||||
|
const { data: closing, isLoading: isLoadingClosing } = useSWR(
|
||||||
|
closingId,
|
||||||
|
(id: number) => ClosingApi.getGeneralInfo(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// WORKAROUND - get flock data from closing ID
|
||||||
|
const { data: projectData, isLoading: isLoadingProject } = useSWR(
|
||||||
|
`flock-${closingId}`,
|
||||||
|
() => ProjectFlockApi.getSingle(Number(closingId))
|
||||||
|
);
|
||||||
|
// WORKAROUND - get kandang data from closing ID
|
||||||
|
const { data: kandangData, isLoading: isLoadingKandang } = useSWR(
|
||||||
|
kandangId ? `kandang-${closingId}-${kandangId}` : null,
|
||||||
|
() => ProjectFlockKandangApi.getSingle(Number(kandangId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: salesData, isLoading: isLoadingSales } = useSWR(
|
||||||
|
closingId ? `sales-${closingId}` : null,
|
||||||
|
() => ClosingApi.getPenjualan(Number(closingId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR(
|
||||||
|
closingId ? `hpp-ekspedisi-${closingId}` : null,
|
||||||
|
() => ClosingApi.getHppEkspedisi(Number(closingId))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!closingId) {
|
||||||
|
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 (!isLoadingClosing && (!closing || isResponseError(closing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
isLoadingClosing ||
|
||||||
|
isLoadingSales ||
|
||||||
|
isLoadingHppEkspedisi ||
|
||||||
|
isLoadingProject ||
|
||||||
|
isLoadingKandang;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
|
||||||
|
{!isLoading && isResponseSuccess(closing) && (
|
||||||
|
<ClosingDetail
|
||||||
|
id={Number(closingId)}
|
||||||
|
initialValue={closing.data}
|
||||||
|
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
|
||||||
|
hppExpeditionData={
|
||||||
|
isResponseSuccess(hppEkspedisiData)
|
||||||
|
? hppEkspedisiData.data
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
projectData={
|
||||||
|
isResponseSuccess(projectData) ? projectData.data : undefined
|
||||||
|
}
|
||||||
|
kandangData={
|
||||||
|
isResponseSuccess(kandangData) ? kandangData.data : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingDetailPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ClosingsTable from '@/components/pages/closing/ClosingsTable';
|
||||||
|
|
||||||
|
const Closing = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<ClosingsTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Closing;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { DailyChecklistContent } from '@/figma-make/components/pages/daily-checklist/DailyChecklistContent';
|
||||||
|
|
||||||
|
const DailyChecklistPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<DailyChecklistContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DailyChecklistPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Dashboard as DashboardDailyChecklist } from '@/figma-make/components/pages/dashboard/Dashboard';
|
||||||
|
|
||||||
|
const DailyChecklistDashboardPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<DashboardDailyChecklist />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DailyChecklistDashboardPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { DetailDailyChecklistContent } from '@/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent';
|
||||||
|
|
||||||
|
const ListDailyChecklistDetailPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<DetailDailyChecklistContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListDailyChecklistDetailPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ListDailyChecklistContent } from '@/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent';
|
||||||
|
|
||||||
|
const ListDailyChecklistPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<ListDailyChecklistContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListDailyChecklistPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { MasterAktivitasContent } from '@/figma-make/components/pages/master-data/activity/MasterAktivitasContent';
|
||||||
|
|
||||||
|
const MasterAktivitasPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<MasterAktivitasContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MasterAktivitasPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { MasterConfigurationContent } from '@/figma-make/components/pages/master-data/configuration/MasterConfigurationContent';
|
||||||
|
|
||||||
|
const MasterConfigurationPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<MasterConfigurationContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MasterConfigurationPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { MasterEmployeeContent } from '@/figma-make/components/pages/master-data/employee/MasterEmployeeContent';
|
||||||
|
|
||||||
|
const MasterEmployeePage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<MasterEmployeeContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MasterEmployeePage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { DailyChecklistReportsContent } from '@/figma-make/components/pages/reports/DailyChecklistReportsContent';
|
||||||
|
|
||||||
|
const DailyChecklistReportsPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<DailyChecklistReportsContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DailyChecklistReportsPage;
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
|
import DashboardProduction from '@/components/pages/dashboard/DashboardProduction';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
return (
|
return <DashboardProduction />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<h1 className='text-3xl font-bold text-primary'>Dashboard</h1>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const ExpenseRealization = () => {
|
|||||||
const isExpenseCanBeRealized =
|
const isExpenseCanBeRealized =
|
||||||
isResponseSuccess(expense) &&
|
isResponseSuccess(expense) &&
|
||||||
expense.data.latest_approval.action !== 'REJECTED' &&
|
expense.data.latest_approval.action !== 'REJECTED' &&
|
||||||
expense.data.latest_approval.step_number === 3;
|
expense.data.latest_approval.step_number === 4;
|
||||||
|
|
||||||
if (isResponseSuccess(expense) && !isExpenseCanBeRealized) {
|
if (isResponseSuccess(expense) && !isExpenseCanBeRealized) {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const FinanceAdjust = () => {
|
||||||
|
return <div>Finance Adjust</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceAdjust;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
|
||||||
|
|
||||||
|
const FinanceAddInitialBalancePage = () => {
|
||||||
|
return <FormFinanceAddInitialBalance type='add' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceAddInitialBalancePage;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection';
|
||||||
|
|
||||||
|
const FinanceAddInjectionPage = () => {
|
||||||
|
return <FormFinanceInjection type='add' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceAddInjectionPage;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
|
||||||
|
|
||||||
|
const FinanceAddPage = () => {
|
||||||
|
return <FormFinanceAdd />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceAddPage;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
|
||||||
|
|
||||||
|
const EditFinanceInitialBalancePage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const financeId = searchParams.get('financeId');
|
||||||
|
|
||||||
|
const { data: finance, isLoading: isLoadingFinance } = useSWR(
|
||||||
|
financeId,
|
||||||
|
(id: number) => FinanceApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!financeId) {
|
||||||
|
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 (!isLoadingFinance && (!finance || isResponseError(finance))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFinance && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingFinance && (
|
||||||
|
<FormFinanceAddInitialBalance
|
||||||
|
type='edit'
|
||||||
|
initialValues={isResponseSuccess(finance) ? finance.data : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFinanceInitialBalancePage;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection';
|
||||||
|
|
||||||
|
const EditFinanceInjectionPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const financeId = searchParams.get('financeId');
|
||||||
|
|
||||||
|
const { data: finance, isLoading: isLoadingFinance } = useSWR(
|
||||||
|
financeId,
|
||||||
|
(id: number) => FinanceApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!financeId) {
|
||||||
|
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 (!isLoadingFinance && (!finance || isResponseError(finance))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFinance && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingFinance && (
|
||||||
|
<FormFinanceInjection
|
||||||
|
type='edit'
|
||||||
|
initialValues={isResponseSuccess(finance) ? finance.data : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFinanceInjectionPage;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
|
||||||
|
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
|
||||||
|
|
||||||
|
const EditFinanceTransactionPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const financeId = searchParams.get('financeId');
|
||||||
|
|
||||||
|
const { data: finance, isLoading: isLoadingFinance } = useSWR(
|
||||||
|
financeId,
|
||||||
|
(id: number) => FinanceApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!financeId) {
|
||||||
|
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 (!isLoadingFinance && (!finance || isResponseError(finance))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFinance && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingFinance && (
|
||||||
|
<FormFinanceAdd
|
||||||
|
type='edit'
|
||||||
|
initialValues={isResponseSuccess(finance) ? finance.data : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFinanceTransactionPage;
|
||||||
@@ -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,41 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import FinanceDetail from '@/components/pages/finance/FinanceDetail';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const FinanceDetailPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const financeId = useSearchParams().get('financeId');
|
||||||
|
|
||||||
|
const { data: finance } = useSWR(financeId, () =>
|
||||||
|
FinanceApi.getSingle(Number(financeId))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!financeId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(finance);
|
||||||
|
|
||||||
|
// if (!finance || isResponseError(finance)) {
|
||||||
|
// router.replace('/404');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isResponseSuccess(finance) && <FinanceDetail finance={finance.data} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceDetailPage;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import FinanceTable from '@/components/pages/finance/FinanceTable';
|
||||||
|
|
||||||
|
const Finance = () => {
|
||||||
|
return (
|
||||||
|
<section className='size-full p-6'>
|
||||||
|
<div className='flex flex-row gap-4'></div>
|
||||||
|
<FinanceTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Finance;
|
||||||
+40
-20
@@ -1,32 +1,46 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@plugin "daisyui";
|
@plugin "daisyui";
|
||||||
@import '../styles/daisyui.css';
|
@import '../styles/daisyui.css';
|
||||||
|
@import '../figma-make/styles/theme.css';
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
@plugin "daisyui/theme" {
|
||||||
name: 'lti';
|
name: 'lti';
|
||||||
default: false;
|
default: false;
|
||||||
prefersdark: false;
|
prefersdark: false;
|
||||||
color-scheme: 'light';
|
color-scheme: 'light';
|
||||||
--color-base-100: oklch(98% 0.001 106.423);
|
|
||||||
--color-base-200: oklch(97% 0.001 106.424);
|
/* Primary Colors */
|
||||||
--color-base-300: oklch(92% 0.003 48.717);
|
--color-primary: oklch(39.4% 0.177 301.9);
|
||||||
--color-base-content: oklch(22.389% 0.031 278.072);
|
--color-primary-content: oklch(87.5% 0.038 274.5);
|
||||||
--color-primary: oklch(60% 0.126 221.723);
|
|
||||||
--color-primary-content: oklch(100% 0 0);
|
/* Secondary Colors */
|
||||||
--color-secondary: oklch(52% 0.105 223.128);
|
--color-secondary: oklch(60.1% 0.258 335.7);
|
||||||
--color-secondary-content: oklch(100% 0 0);
|
--color-secondary-content: oklch(99.4% 0.007 337.8);
|
||||||
--color-accent: oklch(45% 0.085 224.283);
|
|
||||||
--color-accent-content: oklch(100% 0 0);
|
/* Accent Colors */
|
||||||
--color-neutral: oklch(39% 0.07 227.392);
|
--color-accent: oklch(76.2% 0.155 170.8);
|
||||||
--color-neutral-content: oklch(100% 0 0);
|
--color-accent-content: oklch(7.2% 0.007 167.6);
|
||||||
--color-info: oklch(58% 0.158 241.966);
|
|
||||||
--color-info-content: oklch(100% 0 0);
|
/* Neutral Colors */
|
||||||
--color-success: oklch(62% 0.194 149.214);
|
--color-neutral: oklch(22.4% 0.032 258.8);
|
||||||
--color-success-content: oklch(100% 0 0);
|
--color-neutral-content: oklch(87.7% 0.016 257);
|
||||||
--color-warning: oklch(85% 0.199 91.936);
|
|
||||||
--color-warning-content: oklch(0% 0 0);
|
/* Base Colors */
|
||||||
--color-error: oklch(57% 0.245 27.325);
|
--color-base-100: oklch(100% 0 0); /* #ffffff */
|
||||||
--color-error-content: oklch(100% 0 0);
|
--color-base-200: oklch(97.2% 0 0); /* #f2f2f2 */
|
||||||
|
--color-base-300: oklch(93.1% 0.002 249.7); /* #e5e6e6 */
|
||||||
|
--color-base-content: oklch(18.6% 0.024 257.7); /* #1f2937 */
|
||||||
|
|
||||||
|
/* Status/Utility Colors */
|
||||||
|
--color-info: oklch(67.4% 0.176 238.9);
|
||||||
|
--color-info-content: oklch(0% 0 0); /* #000000 */
|
||||||
|
--color-success: oklch(62.3% 0.147 149);
|
||||||
|
--color-success-content: oklch(100% 0 0); /* #ffffff */
|
||||||
|
--color-warning: oklch(82.2% 0.165 91.9);
|
||||||
|
--color-warning-content: oklch(0% 0 0); /* #000000 */
|
||||||
|
--color-error: oklch(61.8% 0.203 27.8);
|
||||||
|
--color-error-content: oklch(100% 0 0); /* #fffffff */
|
||||||
|
|
||||||
--radius-selector: 0rem;
|
--radius-selector: 0rem;
|
||||||
--radius-field: 0.25rem;
|
--radius-field: 0.25rem;
|
||||||
--radius-box: 0.25rem;
|
--radius-box: 0.25rem;
|
||||||
@@ -43,6 +57,12 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-inter: var(--font-inter);
|
--font-inter: var(--font-inter);
|
||||||
|
|
||||||
|
--container-sm: 40rem;
|
||||||
|
--container-md: 48rem;
|
||||||
|
--container-lg: 64rem;
|
||||||
|
--container-xl: 80rem;
|
||||||
|
--container-2xl: 96rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ const DetailInventoryAdjustment = () => {
|
|||||||
|
|
||||||
// Ambil data dari router state
|
// Ambil data dari router state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Router State');
|
|
||||||
console.log(window.history.state);
|
|
||||||
const state = window.history.state?.usr as
|
const state = window.history.state?.usr as
|
||||||
| { inventoryAdjustment?: InventoryAdjustment }
|
| { inventoryAdjustment?: InventoryAdjustment }
|
||||||
| undefined;
|
| undefined;
|
||||||
@@ -26,9 +24,6 @@ const DetailInventoryAdjustment = () => {
|
|||||||
|
|
||||||
const finalData = inventoryAdjustment;
|
const finalData = inventoryAdjustment;
|
||||||
|
|
||||||
console.log('Final Data');
|
|
||||||
console.log(finalData);
|
|
||||||
|
|
||||||
if (!finalData) {
|
if (!finalData) {
|
||||||
return (
|
return (
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
|||||||
@@ -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,50 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import InventoryProductDetail from '@/components/pages/inventory/product/detail/InventoryProductDetail';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { InventoryProductApi } from '@/services/api/inventory';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const InventoryProductDetailPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const inventoryProductId = searchParams.get('inventoryProductId');
|
||||||
|
|
||||||
|
const { data: inventoryProduct, isLoading: isLoadingInventoryProduct } =
|
||||||
|
useSWR(inventoryProductId, (id: number) =>
|
||||||
|
InventoryProductApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!inventoryProductId) {
|
||||||
|
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 (
|
||||||
|
!isLoadingInventoryProduct &&
|
||||||
|
(!inventoryProduct || isResponseError(inventoryProduct))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='size-full'>
|
||||||
|
{isLoadingInventoryProduct && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingInventoryProduct && isResponseSuccess(inventoryProduct) && (
|
||||||
|
<InventoryProductDetail inventoryProduct={inventoryProduct.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InventoryProductDetailPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import InventoryProductTable from '@/components/pages/inventory/product/InventoryProductTable';
|
||||||
|
|
||||||
|
const InventoryProductPage = () => {
|
||||||
|
return (
|
||||||
|
<div className='size-full'>
|
||||||
|
<InventoryProductTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InventoryProductPage;
|
||||||
@@ -3,6 +3,7 @@ import { Inter } from 'next/font/google';
|
|||||||
import '@/app/globals.css';
|
import '@/app/globals.css';
|
||||||
|
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
import { Toaster as SonnerToaster } from '@/figma-make/components/base/sonner';
|
||||||
import MainDrawer from '@/components/MainDrawer';
|
import MainDrawer from '@/components/MainDrawer';
|
||||||
import RequireAuth from '@/components/helper/RequireAuth';
|
import RequireAuth from '@/components/helper/RequireAuth';
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export default function RootLayout({
|
|||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
<SonnerToaster position='top-right' />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ const Marketing = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Marketing;
|
export default Marketing;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
|
||||||
|
|
||||||
|
const AddProductionStandardPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProductionStandardForm formType='add' />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddProductionStandardPage;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditProductionStandardPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// Get Query Params
|
||||||
|
const productionStandardId = searchParams.get('productionStandardId');
|
||||||
|
|
||||||
|
// Fetch Data
|
||||||
|
const { data: productionStandard, isLoading: isLoadingProductionStandard } =
|
||||||
|
useSWR(productionStandardId, (id: number) =>
|
||||||
|
ProductionStandardApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!productionStandardId) {
|
||||||
|
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 (
|
||||||
|
!isLoadingProductionStandard &&
|
||||||
|
(!productionStandard || isResponseError(productionStandard))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoadingProductionStandard && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingProductionStandard &&
|
||||||
|
isResponseSuccess(productionStandard) && (
|
||||||
|
<ProductionStandardForm
|
||||||
|
formType='edit'
|
||||||
|
initialValue={productionStandard.data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditProductionStandardPage;
|
||||||
@@ -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,56 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const DetailProductionStandardPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// Get Query Params
|
||||||
|
const productionStandardId = searchParams.get('productionStandardId');
|
||||||
|
|
||||||
|
// Fetch Data
|
||||||
|
const { data: productionStandard, isLoading: isLoadingProductionStandard } =
|
||||||
|
useSWR(productionStandardId, (id: number) =>
|
||||||
|
ProductionStandardApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!productionStandardId) {
|
||||||
|
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 (
|
||||||
|
!isLoadingProductionStandard &&
|
||||||
|
(!productionStandard || isResponseError(productionStandard))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoadingProductionStandard && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingProductionStandard &&
|
||||||
|
isResponseSuccess(productionStandard) && (
|
||||||
|
<ProductionStandardForm
|
||||||
|
formType='detail'
|
||||||
|
initialValue={productionStandard.data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailProductionStandardPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable';
|
||||||
|
|
||||||
|
const ProductionStandardPage = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
<ProductionStandardTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductionStandardPage;
|
||||||
+21
-3
@@ -1,11 +1,29 @@
|
|||||||
import { redirect } from 'next/navigation';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
import { redirectToSSO } from '@/lib/auth-helper';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
redirect('/dashboard');
|
const { user, isLoadingUser } = useAuth();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pathname === '/') {
|
||||||
|
router.replace('/dashboard');
|
||||||
|
}
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
if (isLoadingUser) {
|
||||||
return (
|
return (
|
||||||
<main className='w-full h-full min-h-screen flex flex-row justify-center items-center'>
|
<main className='w-full h-full min-h-screen flex flex-row justify-center items-center'>
|
||||||
<h1>LTI ERP</h1>
|
<span className='loading loading-spinner loading-lg'></span>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <>Loading...</>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
|
import React, { useImperativeHandle } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const AddProjectFlock = () => {
|
const AddProjectFlock = () => {
|
||||||
|
// useImperativeHandle(ref, () => ({
|
||||||
|
// validate() {
|
||||||
|
// toast.success('Validating');
|
||||||
|
// return false;
|
||||||
|
// },
|
||||||
|
// }));
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-4 flex flex-row justify-center'>
|
<section className='w-full flex flex-row justify-center'>
|
||||||
<ProjectFlockForm formType='add' />
|
<ProjectFlockForm formType='add' />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function AddChickinKandang() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full p-4'>
|
<section className='size-full'>
|
||||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
isResponseSuccess(projectFlockKandang) &&
|
isResponseSuccess(projectFlockKandang) &&
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
|
||||||
import ProjectFlockChickinDetail from '@/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail';
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
const AddChickin = () => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const projectFlockId = searchParams.get('projectFlockId');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section className='w-full p-4'>
|
|
||||||
<ProjectFlockChickinDetail projectFlockId={Number(projectFlockId)} />
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddChickin;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import ChickinTable from '@/components/pages/production/chickin/ChickinTable';
|
|
||||||
|
|
||||||
const Chickin = () => {
|
|
||||||
return (
|
|
||||||
<section className='w-full p-4'>
|
|
||||||
<ChickinTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default Chickin;
|
|
||||||
@@ -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,63 @@
|
|||||||
|
'use client';
|
||||||
|
import ProjectFlockClosingForm from '@/components/pages/production/project-flock/closing/ProjectFlockClosingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const ProjectFlockClosingPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
const projectFlockKandangId = searchParams.get('projectFlockKandangId');
|
||||||
|
|
||||||
|
const { data: projectFlockKandang, isLoading: isLoadingProjectFlockKandang } =
|
||||||
|
useSWR(`get-flock-kandang-id/${projectFlockKandangId}`, () =>
|
||||||
|
ProjectFlockKandangApi.getSingle(parseInt(projectFlockKandangId ?? ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
||||||
|
`get-flock-id/${projectFlockId}`,
|
||||||
|
() => ProjectFlockApi.getSingle(parseInt(projectFlockId ?? ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!projectFlockId || !projectFlockKandangId) {
|
||||||
|
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 (
|
||||||
|
!isLoadingProjectFlock &&
|
||||||
|
(!projectFlock || isResponseError(projectFlock)) &&
|
||||||
|
!isLoadingProjectFlockKandang &&
|
||||||
|
(!projectFlockKandang || isResponseError(projectFlockKandang))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full flex flex-col justify-center'>
|
||||||
|
{isLoadingProjectFlock ||
|
||||||
|
(isLoadingProjectFlockKandang && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
))}
|
||||||
|
{isResponseSuccess(projectFlock) &&
|
||||||
|
isResponseSuccess(projectFlockKandang) && (
|
||||||
|
<ProjectFlockClosingForm
|
||||||
|
projectFlock={projectFlock.data}
|
||||||
|
projectFlockKandang={projectFlockKandang.data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectFlockClosingPage;
|
||||||
@@ -37,7 +37,7 @@ const ProjectFlockEdit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-col justify-center'>
|
<div className='w-full flex flex-col justify-center'>
|
||||||
{isLoadingProjectFlock && (
|
{isLoadingProjectFlock && (
|
||||||
<span className='loading loading-spinner loading-xl' />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import ProjectFlockDetail from '@/components/pages/production/project-flock/detail/ProjectFlockDetail';
|
||||||
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const ProjectFlockDetail = () => {
|
const ProjectFlockDetailPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
@@ -37,19 +38,17 @@ const ProjectFlockDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-col justify-center'>
|
<div className='w-full h-full flex flex-col justify-center'>
|
||||||
{isLoadingProjectFlock && (
|
{isLoadingProjectFlock && (
|
||||||
<span className='loading loading-spinner loading-xl' />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
)}
|
)}
|
||||||
{isResponseSuccess(projectFlock) && (
|
{isResponseSuccess(projectFlock) && (
|
||||||
<ProjectFlockForm
|
<ProjectFlockDetail projectFlock={projectFlock.data} />
|
||||||
formType='detail'
|
|
||||||
initialValues={projectFlock.data}
|
|
||||||
refreshProjectFlocks={refreshProjectFlock}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectFlockDetail;
|
export default ProjectFlockDetailPage;
|
||||||
|
ProjectFlockDetail;
|
||||||
|
ProjectFlockDetail;
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import Drawer from '@/components/Drawer';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
|
||||||
|
export default function ProjectFlockLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const toggleValidate = useUiStore((s) => s.toggleValidate);
|
||||||
|
|
||||||
|
const isAdd = pathname.includes('/add');
|
||||||
|
const isEdit = pathname.includes('/detail/edit');
|
||||||
|
const isDetail = pathname.includes('/detail');
|
||||||
|
const isChickin = pathname.includes('/chickin/add/kandang');
|
||||||
|
const isClosing = pathname.includes('/closing');
|
||||||
|
|
||||||
|
const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing;
|
||||||
|
|
||||||
|
const handleBackdropClick = () => {
|
||||||
|
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
||||||
|
if (isValid) {
|
||||||
|
unsub(); // berhenti listen
|
||||||
|
router.push('/production/project-flock');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleValidate();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* List page always rendered */}
|
||||||
|
<div className='min-h-sceen w-full relative'>
|
||||||
|
<ProjectFlockTable
|
||||||
|
refresh={() => !isOpen && router.push('/production/project-flock')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Render Drawer only on /add */}
|
||||||
|
<Drawer
|
||||||
|
open={isOpen}
|
||||||
|
setOpen={(v) => {
|
||||||
|
if (!v) router.push('/production/project-flock');
|
||||||
|
}}
|
||||||
|
closeOnBackdropClick={isDetail ? true : false}
|
||||||
|
onBackdropClick={handleBackdropClick}
|
||||||
|
variant='right'
|
||||||
|
zIndex='99999'
|
||||||
|
sidebarContent={isOpen && <div className=''>{children}</div>}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import ProjectFlockTable from '@/components/pages/production/project-flock/Proje
|
|||||||
|
|
||||||
const ProjectFlock = () => {
|
const ProjectFlock = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-4'>
|
<section className='size-full p-4'>
|
||||||
<ProjectFlockTable />
|
<ProjectFlockTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const RecordingEdit = () => {
|
|||||||
|
|
||||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
recordingId,
|
recordingId,
|
||||||
(id: number) => RecordingApi.getSingle(id) // Gunakan RecordingApi
|
(id: string) => RecordingApi.getSingle(parseInt(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!recordingId) {
|
if (!recordingId) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const RecordingDetail = () => {
|
|||||||
|
|
||||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
recordingId,
|
recordingId,
|
||||||
(id: number) => RecordingApi.getSingle(id)
|
(id: string) => RecordingApi.getSingle(parseInt(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!recordingId) {
|
if (!recordingId) {
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
'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;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
'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;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
'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,7 @@
|
|||||||
|
import UniformityForm from '@/components/pages/production/uniformity/form/UniformityForm';
|
||||||
|
|
||||||
|
const AddUniformity = () => {
|
||||||
|
return <UniformityForm formType='add' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUniformity;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import UniformityDetail from '@/components/pages/production/uniformity/detail/UniformityDetail';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { UniformityApi } from '@/services/api/uniformity';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const UniformityDetailPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const uniformityId = searchParams.get('uniformityId');
|
||||||
|
|
||||||
|
const { data: uniformity, isLoading: isLoadingUniformity } = useSWR(
|
||||||
|
uniformityId,
|
||||||
|
(id: string) => UniformityApi.getUniformityDetail(parseInt(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uniformityId) {
|
||||||
|
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 (!isLoadingUniformity && (!uniformity || isResponseError(uniformity))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full flex flex-col justify-center'>
|
||||||
|
{isLoadingUniformity && (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4 min-h-screen'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isResponseSuccess(uniformity) && (
|
||||||
|
<UniformityDetail initialValues={uniformity.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityDetailPage;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import UniformityPageWrapper from '@/components/pages/production/uniformity/UniformityPageWrapper';
|
||||||
|
|
||||||
|
export default function UniformityLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return <UniformityPageWrapper>{children}</UniformityPageWrapper>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import UniformityTable from '@/components/pages/production/uniformity/UniformityTable';
|
||||||
|
|
||||||
|
const Uniformity = () => {
|
||||||
|
return <UniformityTable />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Uniformity;
|
||||||
@@ -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,5 @@
|
|||||||
|
const ReportExpenseDetail = () => {
|
||||||
|
return <div>ReportExpenseDetail</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportExpenseDetail;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ReportExpenseTable from '@/components/pages/report/expense/ReportExpenseTable';
|
||||||
|
|
||||||
|
const ReportExpense = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
<ReportExpenseTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportExpense;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import FinanceTabs from '@/components/pages/report/finance/FinanceTabs';
|
||||||
|
|
||||||
|
const Finance = () => {
|
||||||
|
return <FinanceTabs />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Finance;
|
||||||
@@ -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,7 @@
|
|||||||
|
import LogisticStockTabs from '@/components/pages/report/logistic-stock/LogisticStockTabs';
|
||||||
|
|
||||||
|
const LogisticStock = () => {
|
||||||
|
return <LogisticStockTabs />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogisticStock;
|
||||||
@@ -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,11 @@
|
|||||||
|
import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
|
||||||
|
|
||||||
|
const MarketingReportPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<MarketingReportContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketingReportPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent';
|
||||||
|
|
||||||
|
const ProductionResultReportPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full max-w-7xl pb-16'>
|
||||||
|
<ProductionResultContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductionResultReportPage;
|
||||||
+34
-14
@@ -3,29 +3,25 @@
|
|||||||
import { HTMLAttributes, ReactNode } from 'react';
|
import { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import type { Color, Variant, Size } from '@/types/theme';
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends Omit<HTMLAttributes<HTMLSpanElement>, 'className'> {
|
extends Omit<HTMLAttributes<HTMLSpanElement>, 'className'> {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
className?: {
|
className?: {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
variant?: 'default' | 'outline' | 'ghost' | 'soft' | 'dash';
|
statusIndicator?: boolean;
|
||||||
color?:
|
variant?: Variant;
|
||||||
| 'neutral'
|
color?: Color;
|
||||||
| 'primary'
|
size?: Size;
|
||||||
| 'secondary'
|
|
||||||
| 'accent'
|
|
||||||
| 'info'
|
|
||||||
| 'success'
|
|
||||||
| 'warning'
|
|
||||||
| 'error';
|
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Badge = ({
|
const Badge = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
statusIndicator = false,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
color,
|
color,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
@@ -34,7 +30,7 @@ const Badge = ({
|
|||||||
const getBadgeClasses = () => {
|
const getBadgeClasses = () => {
|
||||||
const baseClasses = 'badge';
|
const baseClasses = 'badge';
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses: Record<Variant, string> = {
|
||||||
default: '',
|
default: '',
|
||||||
outline: 'badge-outline',
|
outline: 'badge-outline',
|
||||||
ghost: 'badge-ghost',
|
ghost: 'badge-ghost',
|
||||||
@@ -42,7 +38,7 @@ const Badge = ({
|
|||||||
dash: 'badge-dash',
|
dash: 'badge-dash',
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorClasses = {
|
const colorClasses: Record<Color, string> = {
|
||||||
neutral: 'badge-neutral',
|
neutral: 'badge-neutral',
|
||||||
primary: 'badge-primary',
|
primary: 'badge-primary',
|
||||||
secondary: 'badge-secondary',
|
secondary: 'badge-secondary',
|
||||||
@@ -51,9 +47,10 @@ const Badge = ({
|
|||||||
success: 'badge-success',
|
success: 'badge-success',
|
||||||
warning: 'badge-warning',
|
warning: 'badge-warning',
|
||||||
error: 'badge-error',
|
error: 'badge-error',
|
||||||
|
none: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses: Record<Size, string> = {
|
||||||
xs: 'badge-xs',
|
xs: 'badge-xs',
|
||||||
sm: 'badge-sm',
|
sm: 'badge-sm',
|
||||||
md: 'badge-md',
|
md: 'badge-md',
|
||||||
@@ -70,8 +67,31 @@ const Badge = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStatusClasses = () => {
|
||||||
|
if (!statusIndicator) return '';
|
||||||
|
|
||||||
|
const statusIndicatorClasses: Record<Color, string> = {
|
||||||
|
neutral: 'bg-neutral',
|
||||||
|
primary: 'bg-primary',
|
||||||
|
secondary: 'bg-secondary',
|
||||||
|
accent: 'bg-accent',
|
||||||
|
info: 'bg-info',
|
||||||
|
success: 'bg-success',
|
||||||
|
warning: 'bg-warning',
|
||||||
|
error: 'bg-error',
|
||||||
|
none: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
'w-2.5 h-2.5 rounded-full',
|
||||||
|
color && statusIndicatorClasses[color],
|
||||||
|
className?.status
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={getBadgeClasses()} {...props}>
|
<span className={getBadgeClasses()} {...props}>
|
||||||
|
{statusIndicator && <span className={getStatusClasses()} />}
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { HTMLAttributes, ReactNode, useState } from 'react';
|
|||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Collapse from './Collapse';
|
import Collapse from '@/components/Collapse';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
export interface CardProps
|
export interface CardProps
|
||||||
|
|||||||
+137
-6
@@ -10,28 +10,115 @@ interface DrawerProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (newOpenState: boolean) => void;
|
setOpen: (newOpenState: boolean) => void;
|
||||||
openOnLarge?: boolean;
|
openOnLarge?: boolean;
|
||||||
|
variant?: 'sidebar' | 'left' | 'right';
|
||||||
|
zIndex?: string;
|
||||||
|
className?: DrawerClassName;
|
||||||
|
onBackdropClick?: () => void;
|
||||||
|
closeOnBackdropClick?: boolean;
|
||||||
|
expandedContent?: ReactNode;
|
||||||
|
expandedWidth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DrawerClassName = {
|
||||||
|
drawer?: string;
|
||||||
|
drawerContent?: string;
|
||||||
|
drawerSide?: string;
|
||||||
|
drawerOverlay?: string;
|
||||||
|
drawerSidebarContent?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const Drawer = ({
|
const Drawer = ({
|
||||||
children,
|
children,
|
||||||
sidebarContent,
|
sidebarContent,
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
openOnLarge,
|
openOnLarge,
|
||||||
|
variant = 'sidebar',
|
||||||
|
zIndex = '20',
|
||||||
|
className,
|
||||||
|
onBackdropClick,
|
||||||
|
closeOnBackdropClick = true,
|
||||||
|
expandedContent,
|
||||||
|
expandedWidth = 'w-[400px]',
|
||||||
}: DrawerProps) => {
|
}: DrawerProps) => {
|
||||||
|
const getDrawerClassNames = (): DrawerClassName => {
|
||||||
|
const baseClassNames = {
|
||||||
|
drawer: 'drawer',
|
||||||
|
drawerContent: 'drawer-content',
|
||||||
|
drawerSide: 'drawer-side',
|
||||||
|
drawerOverlay: 'drawer-overlay',
|
||||||
|
drawerSidebarContent: 'min-h-full bg-base-100',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSidebarWidth = () => {
|
||||||
|
if (variant === 'sidebar') {
|
||||||
|
return expandedContent
|
||||||
|
? 'w-full lg:min-w-[600px] lg:max-w-[600px]'
|
||||||
|
: 'w-full max-w-[300px] lg:w-[300px]';
|
||||||
|
}
|
||||||
|
return 'w-full sm:min-w-120 sm:w-fit';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (variant === 'sidebar') {
|
||||||
|
return {
|
||||||
|
...baseClassNames,
|
||||||
|
drawerSidebarContent: cn(
|
||||||
|
baseClassNames.drawerSidebarContent,
|
||||||
|
getSidebarWidth()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else if (variant === 'right') {
|
||||||
|
return {
|
||||||
|
...baseClassNames,
|
||||||
|
drawer: cn(baseClassNames.drawer, 'drawer-end'),
|
||||||
|
drawerSide: cn(
|
||||||
|
baseClassNames.drawerSide,
|
||||||
|
'border-l border-solid border-gray-200 sm:drawer-side w-screen top-0 right-0 fixed z-21'
|
||||||
|
),
|
||||||
|
drawerSidebarContent: cn(
|
||||||
|
baseClassNames.drawerSidebarContent,
|
||||||
|
getSidebarWidth()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else if (variant === 'left') {
|
||||||
|
return {
|
||||||
|
...baseClassNames,
|
||||||
|
drawerSide: cn(
|
||||||
|
baseClassNames.drawerSide,
|
||||||
|
'border-l border-solid border-gray-200 drawer-side w-screen top-0 right-0 fixed z-21'
|
||||||
|
),
|
||||||
|
drawerSidebarContent: cn(
|
||||||
|
baseClassNames.drawerSidebarContent,
|
||||||
|
getSidebarWidth()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return baseClassNames; // Fallback for default or unknown variant
|
||||||
|
};
|
||||||
|
|
||||||
|
const varianClassName = getDrawerClassNames();
|
||||||
|
|
||||||
const toggleDrawer = () => {
|
const toggleDrawer = () => {
|
||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeDrawer = () => {
|
const closeDrawer = () => {
|
||||||
|
if (closeOnBackdropClick) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
}
|
||||||
|
onBackdropClick && onBackdropClick();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('drawer', {
|
className={cn(
|
||||||
|
'drawer',
|
||||||
|
{
|
||||||
'lg:drawer-open': openOnLarge,
|
'lg:drawer-open': openOnLarge,
|
||||||
})}
|
},
|
||||||
|
varianClassName?.drawer,
|
||||||
|
className?.drawer
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
@@ -40,18 +127,62 @@ const Drawer = ({
|
|||||||
className='drawer-toggle'
|
className='drawer-toggle'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='drawer-content'>{children}</div>
|
{/* Drawer Content */}
|
||||||
|
<div
|
||||||
|
className={cn(varianClassName?.drawerContent, className?.drawerContent)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='drawer-side border-r border-solid border-gray-200 z-20'>
|
{/* Drawer Side */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
varianClassName?.drawerSide,
|
||||||
|
className?.drawerSide,
|
||||||
|
zIndex
|
||||||
|
)}
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
aria-label='close sidebar'
|
aria-label='close sidebar'
|
||||||
className='drawer-overlay'
|
className={cn(
|
||||||
|
varianClassName?.drawerOverlay,
|
||||||
|
className?.drawerOverlay
|
||||||
|
)}
|
||||||
onClick={closeDrawer}
|
onClick={closeDrawer}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='min-h-full w-full max-w-[300px] lg:w-[300px] bg-base-100'>
|
{/* Sidebar Content - Full height container */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex h-screen bg-base-100 overflow-hidden',
|
||||||
|
variant === 'right' && 'flex-row'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Primary Sidebar Content */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
varianClassName?.drawerSidebarContent,
|
||||||
|
className?.drawerContent,
|
||||||
|
'overflow-y-auto'
|
||||||
|
)}
|
||||||
|
>
|
||||||
{sidebarContent}
|
{sidebarContent}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded Drawer (Right side, side-by-side) */}
|
||||||
|
{expandedContent && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'border-l border-gray-200 bg-white flex flex-col h-full',
|
||||||
|
expandedWidth
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='overflow-y-auto flex-1 h-full'>
|
||||||
|
{expandedContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { ReactNode, useState, useRef } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export interface DropdownProps {
|
||||||
|
trigger: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
trigger?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
align?: 'start' | 'center' | 'end';
|
||||||
|
direction?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
hover?: boolean;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
close?: boolean;
|
||||||
|
controlled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dropdown = ({
|
||||||
|
trigger,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
align,
|
||||||
|
direction,
|
||||||
|
hover,
|
||||||
|
defaultOpen = false,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
controlled = false,
|
||||||
|
}: DropdownProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
if (!controlled) {
|
||||||
|
const newState = !isOpen;
|
||||||
|
setIsOpen(newState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWrapperClasses = () => {
|
||||||
|
const openState = controlled ? open : isOpen;
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
'dropdown',
|
||||||
|
{
|
||||||
|
'dropdown-start': align === 'start',
|
||||||
|
'dropdown-center': align === 'center',
|
||||||
|
'dropdown-end': align === 'end',
|
||||||
|
'dropdown-top': direction === 'top',
|
||||||
|
'dropdown-bottom': direction === 'bottom',
|
||||||
|
'dropdown-left': direction === 'left',
|
||||||
|
'dropdown-right': direction === 'right',
|
||||||
|
'dropdown-hover': hover,
|
||||||
|
'dropdown-open': openState && !close,
|
||||||
|
'dropdown-close': close,
|
||||||
|
},
|
||||||
|
className?.wrapper
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTriggerClasses = () => {
|
||||||
|
return cn(className?.trigger);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContentClasses = () => {
|
||||||
|
return cn(
|
||||||
|
'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box',
|
||||||
|
className?.content
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (controlled) {
|
||||||
|
return (
|
||||||
|
<div className={getWrapperClasses()}>
|
||||||
|
{trigger}
|
||||||
|
{open && !close && (
|
||||||
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={dropdownRef} className={getWrapperClasses()}>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
className={getTriggerClasses()}
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleDropdown();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trigger}
|
||||||
|
</div>
|
||||||
|
{!close && (
|
||||||
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
|
||||||
|
type FloatingActionsButtonProps = {
|
||||||
|
actions: {
|
||||||
|
action: 'DETAIL' | 'EDIT' | 'DELETE';
|
||||||
|
icon: string;
|
||||||
|
label?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
hidden?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
permissions?: string | string[];
|
||||||
|
}[];
|
||||||
|
approvals: {
|
||||||
|
action: 'APPROVED' | 'REJECTED';
|
||||||
|
icon: string;
|
||||||
|
label?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
permissions?: string | string[];
|
||||||
|
}[];
|
||||||
|
selectedRowIds: number[];
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FloatingActionsButton = ({
|
||||||
|
actions,
|
||||||
|
approvals,
|
||||||
|
selectedRowIds,
|
||||||
|
onClose,
|
||||||
|
}: FloatingActionsButtonProps) => {
|
||||||
|
const { permissionCheck } = useAuth();
|
||||||
|
// Jika tidak ada baris yang dipilih, jangan tampilkan FAB
|
||||||
|
const positionStyles =
|
||||||
|
selectedRowIds.length > 0
|
||||||
|
? 'bottom-[5%] opacity-100'
|
||||||
|
: 'bottom-[-5%] opacity-0';
|
||||||
|
|
||||||
|
// Helper untuk menentukan gaya warna tombol approval
|
||||||
|
const getApprovalColor = (action: 'APPROVED' | 'REJECTED') => {
|
||||||
|
if (action === 'APPROVED') return 'success';
|
||||||
|
if (action === 'REJECTED') return 'error';
|
||||||
|
return 'primary';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionColor = (action: 'DETAIL' | 'EDIT' | 'DELETE') => {
|
||||||
|
if (action === 'DETAIL') return 'white';
|
||||||
|
if (action === 'EDIT') return 'warning';
|
||||||
|
if (action === 'DELETE') return 'error';
|
||||||
|
return 'primary';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
// Container utama FAB
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
`fixed ${positionStyles} inset-x-1/2 -translate-x-1/2 z-50`,
|
||||||
|
'mx-auto w-full max-w-sm sm:mx-0 bg-base-300 p-4 rounded-xl shadow-md transition-all duration-300 transform',
|
||||||
|
'bg-slate-950 backdrop-blur-md'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='flex flex-col gap-3'>
|
||||||
|
{/* === BARIS ATAS: Status Seleksi dan Actions (Termasuk Close) === */}
|
||||||
|
<div className='flex justify-between items-center text-white'>
|
||||||
|
<h4 className='text-base font-semibold'>
|
||||||
|
{selectedRowIds.length} Selected
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className='flex flex-row gap-1 items-stretch'>
|
||||||
|
<div className='flex gap-4 items-center'>
|
||||||
|
{/* Render Aksi dari props.actions */}
|
||||||
|
{actions
|
||||||
|
.filter((action) => {
|
||||||
|
if (action.hidden) return false;
|
||||||
|
if (action.permissions) {
|
||||||
|
if (typeof action.permissions === 'string') {
|
||||||
|
return permissionCheck(action.permissions);
|
||||||
|
}
|
||||||
|
return action.permissions.some((permission) =>
|
||||||
|
permissionCheck(permission)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((action, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={index}
|
||||||
|
onClick={action.onClick}
|
||||||
|
className='text-white hover:text-gray-400 tooltip tooltip-bottom p-0'
|
||||||
|
variant='link'
|
||||||
|
disabled={action.disabled}
|
||||||
|
>
|
||||||
|
<Tooltip content={action.label || action.action}>
|
||||||
|
<Icon
|
||||||
|
icon={action.icon}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className={`text-${getActionColor(action.action)} font-thin`}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className='border-[0.5px] border-white/30 h-full'></div>
|
||||||
|
|
||||||
|
{/* Tombol Close */}
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
className='text-white hover:text-gray-400 p-0'
|
||||||
|
variant='link'
|
||||||
|
>
|
||||||
|
<Tooltip content='Close'>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */}
|
||||||
|
<div className={`grid grid-cols-${approvals.length} gap-3`}>
|
||||||
|
{approvals
|
||||||
|
.filter((approval) => {
|
||||||
|
if (approval.permissions) {
|
||||||
|
if (typeof approval.permissions === 'string') {
|
||||||
|
return permissionCheck(approval.permissions);
|
||||||
|
}
|
||||||
|
return approval.permissions.some((permission) =>
|
||||||
|
permissionCheck(permission)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((approval, index) => (
|
||||||
|
<Button
|
||||||
|
key={index}
|
||||||
|
onClick={approval.onClick}
|
||||||
|
className={cn(
|
||||||
|
'btn btn-lg w-full',
|
||||||
|
'bg-white/20 border-white/30',
|
||||||
|
'text-white/50 font-semibold flex items-center gap-2 rounded-lg transition-all duration-200',
|
||||||
|
approval.disabled
|
||||||
|
? 'cursor-not-allowed'
|
||||||
|
: 'hover:text-white/100 hover:bg-white/40 hover:border-white/50'
|
||||||
|
)}
|
||||||
|
disabled={approval.disabled}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={approval.icon}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className={`text-${getApprovalColor(approval.action)}`}
|
||||||
|
/>
|
||||||
|
{approval.label || approval.action}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FloatingActionsButton;
|
||||||
+21
-147
@@ -1,161 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Drawer from '@/components/Drawer';
|
import Drawer from '@/components/Drawer';
|
||||||
import Menu from '@/components/menu/Menu';
|
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
|
||||||
import Navbar from '@/components/Navbar';
|
import Navbar from '@/components/Navbar';
|
||||||
import Collapse from '@/components/Collapse';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import SidebarMenu from '@/components/molecules/SidebarMenu';
|
||||||
|
import PermissionNotFound from '@/components/helper/PermissionNotFound';
|
||||||
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
||||||
import { cn } from '@/lib/helper';
|
import { isPathActive } from '@/lib/helper';
|
||||||
|
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
|
||||||
type CollapseMenuProps = {
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
title: string;
|
|
||||||
link: string;
|
|
||||||
icon: string;
|
|
||||||
submenu?: CollapseMenuProps[];
|
|
||||||
depth?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPathActive = (pathname: string, link?: string) => {
|
|
||||||
if (!link) return false;
|
|
||||||
|
|
||||||
const splittedPathname = pathname.split('/');
|
|
||||||
const splittedLink = link.split('/');
|
|
||||||
|
|
||||||
const isActiveLinkValid = splittedLink.every((linkChunk, idx) => {
|
|
||||||
return linkChunk === splittedPathname[idx];
|
|
||||||
});
|
|
||||||
|
|
||||||
return pathname.startsWith(link) && isActiveLinkValid;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CollapseMenu = ({
|
|
||||||
title,
|
|
||||||
link,
|
|
||||||
icon,
|
|
||||||
submenu,
|
|
||||||
depth = 0,
|
|
||||||
}: CollapseMenuProps) => {
|
|
||||||
const pathname = usePathname();
|
|
||||||
const isActive = isPathActive(pathname, link);
|
|
||||||
const [open, setOpen] = useState(isActive);
|
|
||||||
|
|
||||||
const menuCollapseTitle = (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'w-full px-3 py-2 rounded-md text-base font-semibold transition-colors flex flex-row justify-between items-center gap-2 hover:bg-primary/10 opacity-40',
|
|
||||||
{
|
|
||||||
'bg-primary/10 opacity-100': open || isActive,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className='flex flex-row items-center gap-2'>
|
|
||||||
<Icon icon={icon} width={20} height={20} />
|
|
||||||
<span>{title}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon='cuida:caret-up-outline'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className={cn('transition-transform', {
|
|
||||||
'rotate-90': !open,
|
|
||||||
'rotate-180': open,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapse
|
|
||||||
open={open}
|
|
||||||
title={menuCollapseTitle}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
className='w-full'
|
|
||||||
titleClassName='w-full p-0!'
|
|
||||||
>
|
|
||||||
<Menu>
|
|
||||||
<div
|
|
||||||
className='w-full py-0.5 flex flex-col gap-0.5'
|
|
||||||
style={{
|
|
||||||
paddingLeft: `${0.5 * (depth + 1)}rem`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{submenu?.map((item, idx) => {
|
|
||||||
const hasSubmenu = item.submenu && item.submenu.length > 0;
|
|
||||||
|
|
||||||
if (!hasSubmenu) {
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
key={idx}
|
|
||||||
title={item.title}
|
|
||||||
href={item.link}
|
|
||||||
icon={item.icon}
|
|
||||||
active={isPathActive(pathname, item.link)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollapseMenu
|
|
||||||
key={idx}
|
|
||||||
title={item.title}
|
|
||||||
link={item.link}
|
|
||||||
icon={item.icon}
|
|
||||||
submenu={item.submenu}
|
|
||||||
depth={depth + 1}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Menu>
|
|
||||||
</Collapse>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MainDrawerMenu = () => {
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
{MAIN_DRAWER_LINKS.map((item, idx) => {
|
|
||||||
const hasSubmenu = item.submenu && item.submenu.length > 0;
|
|
||||||
|
|
||||||
if (!hasSubmenu) {
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
key={idx}
|
|
||||||
title={item.title}
|
|
||||||
href={item.link}
|
|
||||||
icon={item.icon}
|
|
||||||
active={pathname.startsWith(item.link)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollapseMenu
|
|
||||||
key={idx}
|
|
||||||
title={item.title}
|
|
||||||
link={item.link}
|
|
||||||
icon={item.icon}
|
|
||||||
submenu={item.submenu}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MainDrawerContent = () => {
|
const MainDrawerContent = () => {
|
||||||
|
const pathname = usePathname();
|
||||||
const { setMainDrawerOpen } = useUiStore();
|
const { setMainDrawerOpen } = useUiStore();
|
||||||
|
|
||||||
const closeMainDrawerHandler = () => {
|
const closeMainDrawerHandler = () => {
|
||||||
@@ -191,7 +54,7 @@ const MainDrawerContent = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MainDrawerMenu />
|
<SidebarMenu menu={MAIN_DRAWER_LINKS} activeLink={pathname} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -202,6 +65,13 @@ const MainDrawer = ({
|
|||||||
}>) => {
|
}>) => {
|
||||||
const { mainDrawerOpen, setMainDrawerOpen } = useUiStore();
|
const { mainDrawerOpen, setMainDrawerOpen } = useUiStore();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const { permissionCheck } = useAuth();
|
||||||
|
|
||||||
|
const formattedPathname = pathname.endsWith('/') ? pathname : `${pathname}/`;
|
||||||
|
|
||||||
|
const isPermitted = ROUTE_PERMISSIONS[formattedPathname]?.some((permission) =>
|
||||||
|
permissionCheck(permission)
|
||||||
|
);
|
||||||
|
|
||||||
const getPageTitle = useCallback(() => {
|
const getPageTitle = useCallback(() => {
|
||||||
let title = '';
|
let title = '';
|
||||||
@@ -216,9 +86,9 @@ const MainDrawer = ({
|
|||||||
const hasSubmenu = menu?.submenu && menu?.submenu.length > 0;
|
const hasSubmenu = menu?.submenu && menu?.submenu.length > 0;
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
title += menu?.title;
|
title += menu?.text;
|
||||||
} else {
|
} else {
|
||||||
title += ' - ' + menu?.title;
|
title += ' - ' + menu?.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasSubmenu || !menu.submenu) return;
|
if (!hasSubmenu || !menu.submenu) return;
|
||||||
@@ -241,6 +111,10 @@ const MainDrawer = ({
|
|||||||
setMainDrawerOpen(!mainDrawerOpen);
|
setMainDrawerOpen(!mainDrawerOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isPermitted) {
|
||||||
|
return <PermissionNotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
open={mainDrawerOpen}
|
open={mainDrawerOpen}
|
||||||
|
|||||||
+14
-10
@@ -7,6 +7,7 @@ import { Icon } from '@iconify/react';
|
|||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { AuthApi } from '@/services/api/auth';
|
import { AuthApi } from '@/services/api/auth';
|
||||||
@@ -52,21 +53,24 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<div className='dropdown dropdown-end'>
|
<Dropdown
|
||||||
<div
|
align='end'
|
||||||
tabIndex={0}
|
direction='bottom'
|
||||||
role='button'
|
trigger={
|
||||||
className='btn btn-ghost btn-circle avatar'
|
<div className='btn btn-ghost btn-circle avatar'>
|
||||||
>
|
<div className='w-10 rounded-full border flex justify-center items-center'>
|
||||||
<div className='w-10 rounded-full border grid place-items-center'>
|
|
||||||
<Icon icon='uil:user' width={40} height={40} />
|
<Icon icon='uil:user' width={40} height={40} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<Menu className='dropdown-content w-52 mt-3 p-2 bg-base-100 shadow rounded-box menu-sm'>
|
className={{
|
||||||
|
content: 'w-52 mt-3',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu>
|
||||||
<MenuItem title='Logout' onClick={logoutClickHandler} />
|
<MenuItem title='Logout' onClick={logoutClickHandler} />
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+167
-77
@@ -1,7 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
import { ChangeEventHandler, ReactNode } from 'react';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
@@ -17,16 +19,18 @@ const PaginationButton = ({
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}) => (
|
}) => (
|
||||||
<button
|
<Button
|
||||||
className={cn(
|
variant='ghost'
|
||||||
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square',
|
color='none'
|
||||||
'disabled:text-gray-700 disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-gray-50 disabled:active:translate-y-0'
|
|
||||||
)}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
|
'disabled:text-primary disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-primary/10 disabled:active:translate-y-0'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
const EtcPaginationButton = ({
|
const EtcPaginationButton = ({
|
||||||
@@ -48,7 +52,7 @@ const EtcPaginationButton = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role='button'
|
role='button'
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
|
'join-item btn btn-ghost p-2.5 rounded-lg! text-sm font-medium text-gray-500 aspect-square'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
...
|
...
|
||||||
@@ -57,7 +61,7 @@ const EtcPaginationButton = ({
|
|||||||
<div className='dropdown-content'>
|
<div className='dropdown-content'>
|
||||||
<ul
|
<ul
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className='menu bg-base-100 rounded-lg z-1 w-fit min-w-max max-h-64 p-1 shadow-sm mb-2 overflow-y-auto flex-nowrap'
|
className='menu bg-base-100 rounded-lg! z-1 w-fit min-w-max max-h-64 p-1 shadow-sm mb-2 overflow-y-auto flex-nowrap'
|
||||||
>
|
>
|
||||||
{pages.map((pageNumber) => (
|
{pages.map((pageNumber) => (
|
||||||
<li key={pageNumber}>
|
<li key={pageNumber}>
|
||||||
@@ -76,7 +80,7 @@ const EtcPaginationButton = ({
|
|||||||
<button
|
<button
|
||||||
disabled
|
disabled
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
|
'join-item btn btn-ghost p-2.5 rounded-lg! text-sm font-medium text-gray-500 aspect-square'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
...
|
...
|
||||||
@@ -90,16 +94,20 @@ const Pagination = ({
|
|||||||
currentPage = 1,
|
currentPage = 1,
|
||||||
totalItems = 0,
|
totalItems = 0,
|
||||||
itemsPerPage = 10,
|
itemsPerPage = 10,
|
||||||
|
rowOptions = [10, 20, 50, 100],
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onPrevPage = () => {},
|
onPrevPage = () => {},
|
||||||
onNextPage = () => {},
|
onNextPage = () => {},
|
||||||
|
onRowChange,
|
||||||
}: {
|
}: {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
|
rowOptions?: number[];
|
||||||
onPageChange: (pageNumber: number) => void;
|
onPageChange: (pageNumber: number) => void;
|
||||||
onPrevPage: () => void;
|
onPrevPage: () => void;
|
||||||
onNextPage: () => void;
|
onNextPage: () => void;
|
||||||
|
onRowChange?: (row: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const totalPages =
|
const totalPages =
|
||||||
Math.ceil(totalItems / itemsPerPage) === 0
|
Math.ceil(totalItems / itemsPerPage) === 0
|
||||||
@@ -107,30 +115,139 @@ const Pagination = ({
|
|||||||
: Math.ceil(totalItems / itemsPerPage);
|
: Math.ceil(totalItems / itemsPerPage);
|
||||||
|
|
||||||
const pageChangeHandler = (pageNumber: number) => onPageChange(pageNumber);
|
const pageChangeHandler = (pageNumber: number) => onPageChange(pageNumber);
|
||||||
|
const firstPageClickHandler = () => onPageChange(1);
|
||||||
|
const lastPageClickHandler = () => onPageChange(totalPages);
|
||||||
|
|
||||||
return (
|
const rowChangeHandler: ChangeEventHandler<HTMLSelectElement> = (e) => {
|
||||||
<div>
|
onRowChange?.(Number(e.target.value));
|
||||||
<div className='join w-full justify-between items-center gap-3'>
|
};
|
||||||
<button
|
|
||||||
|
const DisplayedRowCountSelect = () => (
|
||||||
|
<div className='flex flex-row items-center gap-4'>
|
||||||
|
<span className='text-sm font-medium text-base-content/50'>Showing</span>
|
||||||
|
|
||||||
|
<select
|
||||||
|
defaultValue={itemsPerPage}
|
||||||
|
onChange={rowChangeHandler}
|
||||||
|
className='select select-xs w-fit pl-3 pr-7 text-base-content/50'
|
||||||
|
>
|
||||||
|
{rowOptions.map((rowOption, rowOptionIdx) => (
|
||||||
|
<option
|
||||||
|
key={rowOptionIdx}
|
||||||
|
value={rowOption}
|
||||||
|
className='text-base-content active:text-neutral-content'
|
||||||
|
>
|
||||||
|
{rowOption} Per page
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const GoToFirstPageButton = () => (
|
||||||
|
<Button
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
onClick={onPrevPage}
|
onClick={firstPageClickHandler}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
className={cn(
|
className={cn(
|
||||||
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs hidden sm:flex justify-center items-center gap-1.5',
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon='uil:arrow-left'
|
icon='heroicons:chevron-double-left'
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className='text-gray-400 group-disabled:text-gray-300'
|
className='text-gray-400 group-disabled:text-gray-300'
|
||||||
/>{' '}
|
/>
|
||||||
Previous
|
</Button>
|
||||||
</button>
|
);
|
||||||
|
|
||||||
{totalPages <= 7 && (
|
const PrevPageButton = () => (
|
||||||
<div className='join-item join gap-0.5'>
|
<Button
|
||||||
{range(1, totalPages).map((pageNumber) => (
|
disabled={currentPage === 1}
|
||||||
|
onClick={onPrevPage}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className={cn(
|
||||||
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
|
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chevron-left'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-gray-400 group-disabled:text-gray-300'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const GoToLastPageButton = () => (
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
onClick={lastPageClickHandler}
|
||||||
|
className={cn(
|
||||||
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
|
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chevron-double-right'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-gray-400 group-disabled:text-gray-300'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const NextPageButton = () => (
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
onClick={onNextPage}
|
||||||
|
className={cn(
|
||||||
|
'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square',
|
||||||
|
'disabled:bg-[initial]! disabled:text-base-content disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chevron-right'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-gray-400 group-disabled:text-gray-300'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PageInfo = () => (
|
||||||
|
<span className='text-nowrap text-sm font-medium text-base-content/50'>
|
||||||
|
Page {currentPage} of {totalPages}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='@container'>
|
||||||
|
<div className='flex flex-row justify-center items-center'>
|
||||||
|
<div className='hidden @md:block'>
|
||||||
|
<DisplayedRowCountSelect />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='join w-full justify-end @md:justify-center items-center gap-0.5'>
|
||||||
|
<div className='hidden @md:block'>
|
||||||
|
<GoToFirstPageButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='hidden @md:block'>
|
||||||
|
<PrevPageButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalPages <= 7 &&
|
||||||
|
range(1, totalPages).map((pageNumber) => (
|
||||||
<PaginationButton
|
<PaginationButton
|
||||||
key={pageNumber}
|
key={pageNumber}
|
||||||
content={pageNumber}
|
content={pageNumber}
|
||||||
@@ -138,11 +255,9 @@ const Pagination = ({
|
|||||||
onClick={() => pageChangeHandler(pageNumber)}
|
onClick={() => pageChangeHandler(pageNumber)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages > 7 && (
|
{totalPages > 7 && (
|
||||||
<div className='join-item join gap-0.5'>
|
<>
|
||||||
<PaginationButton
|
<PaginationButton
|
||||||
content={1}
|
content={1}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
@@ -272,61 +387,36 @@ const Pagination = ({
|
|||||||
onClick={() => pageChangeHandler(totalPages)}
|
onClick={() => pageChangeHandler(totalPages)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<div className='hidden @md:block'>
|
||||||
disabled={currentPage === totalPages}
|
<NextPageButton />
|
||||||
onClick={onNextPage}
|
|
||||||
className={cn(
|
|
||||||
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs hidden sm:flex justify-center items-center gap-1.5',
|
|
||||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Next{' '}
|
|
||||||
<Icon
|
|
||||||
icon='uil:arrow-right'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className='text-gray-400 group-disabled:text-gray-300'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-2 mt-2 sm:hidden'>
|
<div className='hidden @md:block'>
|
||||||
<button
|
<GoToLastPageButton />
|
||||||
disabled={currentPage === 1}
|
</div>
|
||||||
onClick={onPrevPage}
|
</div>
|
||||||
className={cn(
|
|
||||||
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs flex justify-center items-center gap-1.5',
|
|
||||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='uil:arrow-left'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className='text-gray-400 group-disabled:text-gray-300'
|
|
||||||
/>{' '}
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<div className='hidden @md:block'>
|
||||||
disabled={currentPage === totalPages}
|
<PageInfo />
|
||||||
onClick={onNextPage}
|
</div>
|
||||||
className={cn(
|
</div>
|
||||||
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs flex justify-center items-center gap-1.5',
|
|
||||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
<div className='flex @md:hidden flex-col justify-center items-end gap-2'>
|
||||||
)}
|
<div className='flex flex-row items-center gap-0.5'>
|
||||||
>
|
<GoToFirstPageButton />
|
||||||
Next{' '}
|
<PrevPageButton />
|
||||||
<Icon
|
<NextPageButton />
|
||||||
icon='uil:arrow-right'
|
<GoToLastPageButton />
|
||||||
width={20}
|
</div>
|
||||||
height={20}
|
|
||||||
className='text-gray-400 group-disabled:text-gray-300'
|
<div className='flex flex-row items-center gap-4'>
|
||||||
/>
|
<DisplayedRowCountSelect />
|
||||||
</button>
|
|
||||||
|
<PageInfo />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+139
-35
@@ -14,6 +14,7 @@ import {
|
|||||||
SortingState,
|
SortingState,
|
||||||
OnChangeFn,
|
OnChangeFn,
|
||||||
Row,
|
Row,
|
||||||
|
HeaderContext,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { rankItem } from '@tanstack/match-sorter-utils';
|
import { rankItem } from '@tanstack/match-sorter-utils';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -31,6 +32,9 @@ interface TableClassNames {
|
|||||||
tableBodyClassName?: string;
|
tableBodyClassName?: string;
|
||||||
bodyRowClassName?: string;
|
bodyRowClassName?: string;
|
||||||
bodyColumnClassName?: string;
|
bodyColumnClassName?: string;
|
||||||
|
tableFooterClassName?: string;
|
||||||
|
footerRowClassName?: string;
|
||||||
|
footerColumnClassName?: string;
|
||||||
paginationClassName?: string;
|
paginationClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +42,7 @@ export interface TableProps<TData extends object> {
|
|||||||
data: TData[];
|
data: TData[];
|
||||||
columns: ColumnDef<TData, unknown>[];
|
columns: ColumnDef<TData, unknown>[];
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
onPageSizeChange?: (pageSize: number) => void;
|
||||||
totalItems?: number;
|
totalItems?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
onPageChange?: (page: number) => void;
|
onPageChange?: (page: number) => void;
|
||||||
@@ -52,6 +57,15 @@ export interface TableProps<TData extends object> {
|
|||||||
rowSelection?: Record<string, boolean>;
|
rowSelection?: Record<string, boolean>;
|
||||||
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
||||||
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
||||||
|
renderFooter?: boolean;
|
||||||
|
withCheckbox?: boolean;
|
||||||
|
rowOptions?: number[];
|
||||||
|
/**
|
||||||
|
* Custom row renderer. Should return a complete <tr> element or null.
|
||||||
|
* This gives full control over the row structure including colspan.
|
||||||
|
* Return null to render the default row.
|
||||||
|
*/
|
||||||
|
renderCustomRow?: (row: Row<TData>) => ReactNode | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
||||||
@@ -64,28 +78,36 @@ const emptyContentDefaultValue = (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const TABLE_DEFAULT_STYLING = {
|
||||||
|
containerClassName: 'w-full mb-20',
|
||||||
|
tableWrapperClassName:
|
||||||
|
'overflow-x-auto border border-solid border-base-content/10 rounded-lg',
|
||||||
|
tableClassName: 'font-inter w-full table-auto text-sm font-medium',
|
||||||
|
tableHeaderClassName: '',
|
||||||
|
headerRowClassName: '',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-4 py-3 border-base-content/10 text-base-content/50',
|
||||||
|
tableBodyClassName: '',
|
||||||
|
bodyRowClassName: 'border-t border-base-content/10',
|
||||||
|
bodyColumnClassName: 'px-4 py-3 text-base-content',
|
||||||
|
paginationClassName: '',
|
||||||
|
tableFooterClassName: 'font-semibold border-base-content/10',
|
||||||
|
footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10',
|
||||||
|
footerColumnClassName: 'p-4 text-base-content whitespace-nowrap',
|
||||||
|
};
|
||||||
|
|
||||||
const Table = <TData extends object>({
|
const Table = <TData extends object>({
|
||||||
data = [],
|
data = [],
|
||||||
columns = [],
|
columns = [],
|
||||||
pageSize = 10,
|
pageSize = 10,
|
||||||
|
onPageSizeChange,
|
||||||
totalItems,
|
totalItems,
|
||||||
page,
|
page,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
fuzzySearchValue,
|
fuzzySearchValue,
|
||||||
onFuzzySearchValueChange,
|
onFuzzySearchValueChange,
|
||||||
className = {
|
className = TABLE_DEFAULT_STYLING,
|
||||||
containerClassName: '',
|
|
||||||
tableWrapperClassName: '',
|
|
||||||
tableClassName: '',
|
|
||||||
tableHeaderClassName: '',
|
|
||||||
headerRowClassName: '',
|
|
||||||
headerColumnClassName: '',
|
|
||||||
tableBodyClassName: '',
|
|
||||||
bodyRowClassName: '',
|
|
||||||
bodyColumnClassName: '',
|
|
||||||
paginationClassName: '',
|
|
||||||
},
|
|
||||||
emptyContent = emptyContentDefaultValue,
|
emptyContent = emptyContentDefaultValue,
|
||||||
sorting,
|
sorting,
|
||||||
setSorting,
|
setSorting,
|
||||||
@@ -93,12 +115,21 @@ const Table = <TData extends object>({
|
|||||||
rowSelection,
|
rowSelection,
|
||||||
setRowSelection,
|
setRowSelection,
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
|
renderFooter = false,
|
||||||
|
withCheckbox = false,
|
||||||
|
rowOptions = [10, 20, 50, 100],
|
||||||
|
renderCustomRow,
|
||||||
}: TableProps<TData>) => {
|
}: TableProps<TData>) => {
|
||||||
const isServerSideTable =
|
const isServerSideTable =
|
||||||
totalItems !== undefined &&
|
totalItems !== undefined &&
|
||||||
page !== undefined &&
|
page !== undefined &&
|
||||||
onPageChange !== undefined;
|
onPageChange !== undefined;
|
||||||
|
|
||||||
|
const tableClassNames = {
|
||||||
|
...TABLE_DEFAULT_STYLING,
|
||||||
|
...className,
|
||||||
|
};
|
||||||
|
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
@@ -191,37 +222,67 @@ const Table = <TData extends object>({
|
|||||||
}, [pageSize, setPageSize]);
|
}, [pageSize, setPageSize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className.containerClassName}>
|
<div className={tableClassNames.containerClassName}>
|
||||||
<div className={className.tableWrapperClassName}>
|
<div className={tableClassNames.tableWrapperClassName}>
|
||||||
<table className={className.tableClassName}>
|
<table className={tableClassNames.tableClassName}>
|
||||||
<thead className={className.tableHeaderClassName}>
|
<thead className={tableClassNames.tableHeaderClassName}>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id} className={className.headerRowClassName}>
|
<tr
|
||||||
{headerGroup.headers.map((header) => (
|
key={headerGroup.id}
|
||||||
|
className={tableClassNames.headerRowClassName}
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
const columnRelativeDepth =
|
||||||
|
header.depth - header.column.depth;
|
||||||
|
if (
|
||||||
|
!header.isPlaceholder &&
|
||||||
|
columnRelativeDepth > 1 &&
|
||||||
|
header.id === header.column.id
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let rowSpan = 1;
|
||||||
|
if (header.isPlaceholder) {
|
||||||
|
const leafs = header.getLeafHeaders();
|
||||||
|
rowSpan = leafs[leafs.length - 1].depth - header.depth;
|
||||||
|
}
|
||||||
|
return (
|
||||||
<th
|
<th
|
||||||
key={header.id}
|
key={header.id}
|
||||||
colSpan={header.colSpan}
|
colSpan={header.colSpan}
|
||||||
|
rowSpan={rowSpan}
|
||||||
onClick={header.column.getToggleSortingHandler()}
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
className={cn(
|
className={cn(
|
||||||
header.column.getCanSort()
|
header.column.getCanSort()
|
||||||
? 'cursor-pointer select-none'
|
? 'cursor-pointer select-none'
|
||||||
: '',
|
: '',
|
||||||
className.headerColumnClassName
|
{
|
||||||
|
'first:w-9 first:pr-0': withCheckbox,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'border-b': header.colSpan > 1,
|
||||||
|
},
|
||||||
|
tableClassNames.headerColumnClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-1'>
|
<div
|
||||||
|
className={cn('flex items-center gap-1 min-h-full', {
|
||||||
|
'justify-center': header.colSpan > 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{header.column.getCanSort() && (
|
{header.column.getCanSort() && (
|
||||||
<div className='flex items-center'>
|
<div className='w-4 h-4 relative flex flex-col items-center'>
|
||||||
<Icon
|
<Icon
|
||||||
icon='lucide:arrow-up'
|
icon='heroicons:chevron-up-16-solid'
|
||||||
width={12}
|
width={18}
|
||||||
height={12}
|
height={18}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'absolute -top-1',
|
||||||
'transition-all ease-in-out duration-200',
|
'transition-all ease-in-out duration-200',
|
||||||
header.column.getIsSorted() === 'asc'
|
header.column.getIsSorted() === 'asc'
|
||||||
? 'text-black'
|
? 'text-black'
|
||||||
@@ -229,10 +290,11 @@ const Table = <TData extends object>({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
icon='lucide:arrow-down'
|
icon='heroicons:chevron-down-16-solid'
|
||||||
width={12}
|
width={18}
|
||||||
height={12}
|
height={18}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'absolute -bottom-1.5',
|
||||||
'transition-all ease-in-out duration-200',
|
'transition-all ease-in-out duration-200',
|
||||||
header.column.getIsSorted() === 'desc'
|
header.column.getIsSorted() === 'desc'
|
||||||
? 'text-black'
|
? 'text-black'
|
||||||
@@ -243,25 +305,65 @@ const Table = <TData extends object>({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody className={className.tableBodyClassName}>
|
<tbody className={tableClassNames.tableBodyClassName}>
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => {
|
||||||
<tr key={row.id} className={className.bodyRowClassName}>
|
const customRowContent = renderCustomRow?.(row);
|
||||||
|
|
||||||
|
if (customRowContent) {
|
||||||
|
return renderCustomRow?.(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={row.id} className={tableClassNames.bodyRowClassName}>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td key={cell.id} className={className.bodyColumnClassName}>
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className={cn(
|
||||||
|
{ 'first:w-9 first:pr-0': withCheckbox },
|
||||||
|
tableClassNames.bodyColumnClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
flexRender(cell.column.columnDef.cell, cell.getContext())}
|
flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading && <div className='skeleton w-full h-4' />}
|
{isLoading && <div className='skeleton w-full h-4' />}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tfoot className={cn(tableClassNames.tableFooterClassName)}>
|
||||||
|
{renderFooter && (
|
||||||
|
<tr className={cn(tableClassNames.footerRowClassName)}>
|
||||||
|
{table.getAllLeafColumns().map((column) => (
|
||||||
|
<td
|
||||||
|
key={column.id}
|
||||||
|
className={cn(
|
||||||
|
{ 'first:w-9 first:pr-0': withCheckbox },
|
||||||
|
tableClassNames.footerColumnClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{column.columnDef.footer &&
|
||||||
|
flexRender(column.columnDef.footer, {
|
||||||
|
column,
|
||||||
|
header: column.columnDef,
|
||||||
|
table,
|
||||||
|
} as HeaderContext<TData, unknown>)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -270,7 +372,7 @@ const Table = <TData extends object>({
|
|||||||
emptyContent}
|
emptyContent}
|
||||||
|
|
||||||
{data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && (
|
{data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && (
|
||||||
<div className={cn('mt-5', className.paginationClassName)}>
|
<div className={cn('mt-5', tableClassNames.paginationClassName)}>
|
||||||
<Pagination
|
<Pagination
|
||||||
totalItems={isServerSideTable ? totalItems : table.getRowCount()}
|
totalItems={isServerSideTable ? totalItems : table.getRowCount()}
|
||||||
itemsPerPage={table.getState().pagination.pageSize}
|
itemsPerPage={table.getState().pagination.pageSize}
|
||||||
@@ -282,6 +384,8 @@ const Table = <TData extends object>({
|
|||||||
onPrevPage={prevPageClickHandler}
|
onPrevPage={prevPageClickHandler}
|
||||||
onNextPage={nextPageClickHandler}
|
onNextPage={nextPageClickHandler}
|
||||||
onPageChange={pageChangeHandler}
|
onPageChange={pageChangeHandler}
|
||||||
|
rowOptions={rowOptions}
|
||||||
|
onRowChange={onPageSizeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+11
-4
@@ -21,6 +21,7 @@ export interface TabsProps
|
|||||||
className?:
|
className?:
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
|
container?: string;
|
||||||
wrapper?: string;
|
wrapper?: string;
|
||||||
tab?: string;
|
tab?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -53,8 +54,12 @@ const Tabs = ({
|
|||||||
onTabChange?.(tabId);
|
onTabChange?.(tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { wrapper: wrapperClassName, tab: tabClassName } =
|
const {
|
||||||
typeof className === 'object'
|
container: containerClassName,
|
||||||
|
wrapper: wrapperClassName,
|
||||||
|
tab: tabClassName,
|
||||||
|
content: contentClassName,
|
||||||
|
} = typeof className === 'object'
|
||||||
? className
|
? className
|
||||||
: { wrapper: className, tab: undefined };
|
: { wrapper: className, tab: undefined };
|
||||||
|
|
||||||
@@ -104,7 +109,7 @@ const Tabs = ({
|
|||||||
{...props}
|
{...props}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full',
|
'w-full',
|
||||||
typeof className === 'string' ? className : undefined
|
typeof className === 'string' ? className : containerClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div role='tablist' className={getTabsClasses()}>
|
<div role='tablist' className={getTabsClasses()}>
|
||||||
@@ -121,7 +126,9 @@ const Tabs = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeContent && <div className='mt-4'>{activeContent}</div>}
|
{activeContent && (
|
||||||
|
<div className={cn('mt-4', contentClassName)}>{activeContent}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { ReactNode, useState, useRef } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export interface DropdownProps {
|
||||||
|
trigger: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
trigger?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
align?: 'start' | 'center' | 'end';
|
||||||
|
direction?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
hover?: boolean;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
close?: boolean;
|
||||||
|
controlled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dropdown = ({
|
||||||
|
trigger,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
align,
|
||||||
|
direction,
|
||||||
|
hover,
|
||||||
|
defaultOpen = false,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
controlled = false,
|
||||||
|
}: DropdownProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
if (!controlled) {
|
||||||
|
const newState = !isOpen;
|
||||||
|
setIsOpen(newState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWrapperClasses = () => {
|
||||||
|
const openState = controlled ? open : isOpen;
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
'dropdown',
|
||||||
|
{
|
||||||
|
'dropdown-start': align === 'start',
|
||||||
|
'dropdown-center': align === 'center',
|
||||||
|
'dropdown-end': align === 'end',
|
||||||
|
'dropdown-top': direction === 'top',
|
||||||
|
'dropdown-bottom': direction === 'bottom',
|
||||||
|
'dropdown-left': direction === 'left',
|
||||||
|
'dropdown-right': direction === 'right',
|
||||||
|
'dropdown-hover': hover,
|
||||||
|
'dropdown-open': openState && !close,
|
||||||
|
'dropdown-close': close,
|
||||||
|
},
|
||||||
|
className?.wrapper
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTriggerClasses = () => {
|
||||||
|
return cn(className?.trigger);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContentClasses = () => {
|
||||||
|
return cn(
|
||||||
|
'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box',
|
||||||
|
className?.content
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (controlled) {
|
||||||
|
return (
|
||||||
|
<div className={getWrapperClasses()}>
|
||||||
|
{trigger}
|
||||||
|
{open && !close && (
|
||||||
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={dropdownRef} className={getWrapperClasses()}>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
className={getTriggerClasses()}
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleDropdown();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trigger}
|
||||||
|
</div>
|
||||||
|
{!close && (
|
||||||
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const PermissionNotFound = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
|
||||||
|
<h2 className='text-2xl font-bold text-error'>Permission Not Found</h2>
|
||||||
|
<p className='text-gray-600 text-center'>
|
||||||
|
You do not have permission to access this page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PermissionNotFound;
|
||||||
@@ -1,56 +1,84 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode, useEffect } from 'react';
|
import { ReactNode, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import useSWR from 'swr';
|
||||||
import useSWRImmutable from 'swr/immutable';
|
|
||||||
|
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||||
|
import { AuthApi } from '@/services/api/auth';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
|
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { redirectToSSO } from '@/lib/auth-helper';
|
||||||
|
|
||||||
interface RequireAuthProps {
|
interface RequireAuthProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequireAuth = ({ children }: RequireAuthProps) => {
|
const RequireAuth = ({ children }: RequireAuthProps) => {
|
||||||
const router = useRouter();
|
const { user, setUser, setIsLoadingUser } = useAuth();
|
||||||
const { setUser, setIsLoadingUser } = useAuth();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: userResponse,
|
data: userResponse,
|
||||||
isLoading: isLoadingUserResponse,
|
isLoading: isLoadingUserResponse,
|
||||||
error: userErrorResponse,
|
error: userErrorResponse,
|
||||||
} = useSWRImmutable<
|
} = useSWR<
|
||||||
GetMeResponse & { ok?: boolean },
|
GetMeResponse & { ok?: boolean },
|
||||||
AxiosError<BaseApiResponse>,
|
AxiosError<BaseApiResponse>,
|
||||||
SWRHttpKey
|
SWRHttpKey
|
||||||
>('/sso/userinfo', httpClientFetcher, {
|
>('/sso/userinfo', httpClientFetcher, {
|
||||||
shouldRetryOnError: false,
|
shouldRetryOnError: false,
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
refreshInterval: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
// refresh every 12 minutes
|
||||||
setIsLoadingUser(isLoadingUserResponse);
|
refreshInterval: 12 * 60 * 1000,
|
||||||
}, [isLoadingUserResponse, setIsLoadingUser]);
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(userResponse)) {
|
if (isResponseSuccess(userResponse)) {
|
||||||
setUser(userResponse.data);
|
setUser(userResponse.data);
|
||||||
} else if (
|
|
||||||
isResponseError(userErrorResponse?.response?.data) &&
|
|
||||||
typeof window !== 'undefined'
|
|
||||||
) {
|
|
||||||
router.replace(
|
|
||||||
`${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [userResponse, userErrorResponse, setIsLoadingUser, setUser]);
|
}, [userResponse, setUser]);
|
||||||
|
|
||||||
if (isLoadingUserResponse && !userResponse && !userErrorResponse) {
|
// Explicitly handle 401 redirect from the component level
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isResponseError(userResponse) &&
|
||||||
|
userErrorResponse?.response?.status === 401
|
||||||
|
) {
|
||||||
|
// Clear cache to prevent stale data from rendering children
|
||||||
|
// mutate('/sso/userinfo', undefined, { revalidate: false }); // Optional: if using global mutate
|
||||||
|
setUser(undefined);
|
||||||
|
redirectToSSO();
|
||||||
|
}
|
||||||
|
}, [userErrorResponse, setUser, userResponse]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoadingUser(isLoadingUserResponse);
|
||||||
|
}, [isLoadingUserResponse]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(
|
||||||
|
async () => {
|
||||||
|
await AuthApi.refresh();
|
||||||
|
},
|
||||||
|
12 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const refreshUserSession = async () => {
|
||||||
|
await AuthApi.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshUserSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(isLoadingUserResponse && !userResponse && !userErrorResponse) ||
|
||||||
|
(!userResponse && !userErrorResponse)
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
<span className='loading loading-spinner loading-xl' />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
@@ -58,7 +86,22 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{isResponseSuccess(userResponse) && children}</>;
|
if (!isLoadingUserResponse && userErrorResponse) {
|
||||||
|
return (
|
||||||
|
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
|
||||||
|
<h2 className='text-2xl font-bold text-error'>Authentication Failed</h2>
|
||||||
|
<p className='text-gray-600'>
|
||||||
|
Please try refreshing the page or contact support if the problem
|
||||||
|
persists.
|
||||||
|
</p>
|
||||||
|
<button className='btn btn-primary' onClick={() => redirectToSSO()}>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{isResponseSuccess(userResponse) && user && children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequireAuth;
|
export default RequireAuth;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
|
||||||
|
interface RequirePermissionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
permissions: string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const RequirePermission = ({
|
||||||
|
children,
|
||||||
|
permissions,
|
||||||
|
}: RequirePermissionProps) => {
|
||||||
|
const { permissionCheck } = useAuth();
|
||||||
|
|
||||||
|
const isPermitted =
|
||||||
|
typeof permissions === 'string'
|
||||||
|
? permissionCheck(permissions)
|
||||||
|
: permissions.some((permission) => permissionCheck(permission));
|
||||||
|
|
||||||
|
if (!isPermitted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RequirePermission;
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export interface DrawerHeaderProps {
|
||||||
|
// Left side props
|
||||||
|
leftIcon?: string;
|
||||||
|
leftIconSize?: number;
|
||||||
|
leftIconHref?: string;
|
||||||
|
leftIconOnClick?: () => void;
|
||||||
|
leftIconClassName?: string;
|
||||||
|
|
||||||
|
// Subtitle/label props
|
||||||
|
subtitle?: string | ReactNode;
|
||||||
|
subtitleClassName?: string;
|
||||||
|
|
||||||
|
// Right side actions (children)
|
||||||
|
children?: ReactNode;
|
||||||
|
|
||||||
|
// Container props
|
||||||
|
className?: string;
|
||||||
|
showDivider?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DrawerHeader = ({
|
||||||
|
leftIcon = 'mdi:close',
|
||||||
|
leftIconSize = 24,
|
||||||
|
leftIconHref,
|
||||||
|
leftIconOnClick,
|
||||||
|
leftIconClassName,
|
||||||
|
subtitle,
|
||||||
|
subtitleClassName,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showDivider = true,
|
||||||
|
}: DrawerHeaderProps) => {
|
||||||
|
const renderLeftIcon = () => {
|
||||||
|
const iconElement = (
|
||||||
|
<Icon
|
||||||
|
icon={leftIcon}
|
||||||
|
width={leftIconSize}
|
||||||
|
height={leftIconSize}
|
||||||
|
className={cn('cursor-pointer', leftIconClassName)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (leftIconHref) {
|
||||||
|
return (
|
||||||
|
<Link href={leftIconHref} className='hover:text-gray-400'>
|
||||||
|
{iconElement}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftIconOnClick) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={leftIconOnClick}
|
||||||
|
className='hover:text-gray-400 bg-transparent border-none p-0'
|
||||||
|
>
|
||||||
|
{iconElement}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-row justify-between items-center px-4 pt-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Left Side */}
|
||||||
|
<div className='flex flex-row h-full gap-2 items-center'>
|
||||||
|
{renderLeftIcon()}
|
||||||
|
|
||||||
|
{showDivider && subtitle && (
|
||||||
|
<div className='divider divider-horizontal p-0 m-0'></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subtitle && (
|
||||||
|
<div className={cn('text-sm text-neutral', subtitleClassName)}>
|
||||||
|
{subtitle}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side Actions */}
|
||||||
|
{children && (
|
||||||
|
<div className='flex flex-row gap-3 justify-end items-center'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DrawerHeader;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import Alert from '@/components/Alert';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alert Unique Error List
|
||||||
|
* @param formErrorList - Array of error messages
|
||||||
|
* @param onClose - Function to close the alert
|
||||||
|
*/
|
||||||
|
const AlertErrorList = ({
|
||||||
|
formErrorList,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
formErrorList: string[];
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
if (formErrorList.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert color='error' className='w-full flex flex-col gap-2 px-4 m-4'>
|
||||||
|
<div className='flex justify-between items-center gap-2 w-full'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Icon icon='material-symbols:error-outline' width={24} height={24} />
|
||||||
|
<span className='font-semibold'>
|
||||||
|
Terdapat {formErrorList.length} error pada form:
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
variant='link'
|
||||||
|
className='ml-auto p-0 w-fit text-white'
|
||||||
|
color='none'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ul className='list-disc list-inside pl-8 space-y-1 w-full'>
|
||||||
|
{formErrorList.map((error, index) => (
|
||||||
|
<li key={index} className='text-sm'>
|
||||||
|
{error}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlertErrorList;
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import { HTMLProps, useEffect, useRef } from 'react';
|
import { HTMLProps, useEffect, useRef } from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import { Size } from '@/types/theme';
|
||||||
|
|
||||||
interface CheckboxInputProps extends HTMLProps<HTMLInputElement> {
|
interface CheckboxInputProps extends Omit<HTMLProps<HTMLInputElement>, 'size'> {
|
||||||
name: string;
|
name: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
indeterminate?: boolean;
|
indeterminate?: boolean;
|
||||||
@@ -16,6 +17,7 @@ interface CheckboxInputProps extends HTMLProps<HTMLInputElement> {
|
|||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
isValid?: boolean;
|
isValid?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
size?: Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckboxInput = ({
|
const CheckboxInput = ({
|
||||||
@@ -27,10 +29,19 @@ const CheckboxInput = ({
|
|||||||
isValid,
|
isValid,
|
||||||
isError,
|
isError,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
size = 'sm',
|
||||||
...rest
|
...rest
|
||||||
}: CheckboxInputProps) => {
|
}: CheckboxInputProps) => {
|
||||||
const ref = useRef<HTMLInputElement>(null!);
|
const ref = useRef<HTMLInputElement>(null!);
|
||||||
|
|
||||||
|
const checkboxBaseClassName = cn('checkbox cursor-pointer rounded-md', {
|
||||||
|
'checkbox-xs': size === 'xs',
|
||||||
|
'checkbox-sm': size === 'sm',
|
||||||
|
'checkbox-md': size === 'md',
|
||||||
|
'checkbox-lg': size === 'lg',
|
||||||
|
'checkbox-xl': size === 'xl',
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof indeterminate === 'boolean') {
|
if (typeof indeterminate === 'boolean') {
|
||||||
ref.current.indeterminate = !rest.checked && indeterminate;
|
ref.current.indeterminate = !rest.checked && indeterminate;
|
||||||
@@ -53,7 +64,7 @@ const CheckboxInput = ({
|
|||||||
id={name}
|
id={name}
|
||||||
name={name}
|
name={name}
|
||||||
className={cn(
|
className={cn(
|
||||||
'checkbox cursor-pointer',
|
checkboxBaseClassName,
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success': isValid,
|
'border-success': isValid,
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import Modal, { useModal } from '../Modal';
|
|
||||||
import { DateRange, DayPicker, Matcher } from 'react-day-picker';
|
import { DateRange, DayPicker, Matcher } from 'react-day-picker';
|
||||||
import 'react-day-picker/dist/style.css';
|
import 'react-day-picker/dist/style.css';
|
||||||
import Button from '../Button';
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
export interface DateInputProps {
|
export interface DateInputProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ const DebouncedTextInput = (props: DebouncedTextInputProps) => {
|
|||||||
setInternalChangeEvent(e);
|
setInternalChangeEvent(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sync internal value with external value prop changes (e.g., from reset)
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalValue(props.value);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debouncedChangeEvent) {
|
if (debouncedChangeEvent) {
|
||||||
onChange?.(debouncedChangeEvent);
|
onChange?.(debouncedChangeEvent);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const FileInput = ({
|
|||||||
isError,
|
isError,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
required = false,
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
@@ -56,6 +57,13 @@ const FileInput = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
{required && (
|
||||||
|
<>
|
||||||
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
|
<span className='text-error'> *</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, ReactNode } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
ReactNode,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
} from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
export interface RadioOption {
|
export interface RadioOption {
|
||||||
@@ -8,37 +13,74 @@ export interface RadioOption {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RadioInputProps {
|
// DaisyUI Radio Colors
|
||||||
label?: string;
|
export type RadioColor =
|
||||||
bottomLabel?: string;
|
| 'neutral'
|
||||||
|
| 'primary'
|
||||||
|
| 'secondary'
|
||||||
|
| 'accent'
|
||||||
|
| 'success'
|
||||||
|
| 'warning'
|
||||||
|
| 'info'
|
||||||
|
| 'error';
|
||||||
|
|
||||||
|
// DaisyUI Radio Sizes
|
||||||
|
export type RadioSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
|
||||||
|
// Context untuk RadioGroup
|
||||||
|
interface RadioGroupContextValue {
|
||||||
name: string;
|
name: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
options: RadioOption[];
|
color?: RadioColor;
|
||||||
variant?: string;
|
size?: RadioSize;
|
||||||
className?: {
|
|
||||||
wrapper?: string;
|
|
||||||
label?: string;
|
|
||||||
radioWrapper?: string;
|
|
||||||
radio?: string;
|
|
||||||
};
|
|
||||||
isError?: boolean;
|
|
||||||
isValid?: boolean;
|
|
||||||
errorMessage?: string;
|
|
||||||
required?: boolean;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
startAdornment?: ReactNode;
|
|
||||||
endAdornment?: ReactNode;
|
|
||||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RadioInput = ({
|
const RadioGroupContext = createContext<RadioGroupContextValue | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const useRadioGroup = () => {
|
||||||
|
const context = useContext(RadioGroupContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('RadioGroupItem must be used within RadioGroup');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
// RadioGroup Component
|
||||||
|
export interface RadioGroupProps {
|
||||||
|
label?: string;
|
||||||
|
bottomLabel?: string;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
options?: RadioOption[];
|
||||||
|
color?: RadioColor;
|
||||||
|
size?: RadioSize;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
label?: string;
|
||||||
|
radioWrapper?: string;
|
||||||
|
};
|
||||||
|
isError?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadioGroup = ({
|
||||||
label,
|
label,
|
||||||
bottomLabel,
|
bottomLabel,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
variant = 'radio-primary',
|
color = 'primary',
|
||||||
|
size = 'md',
|
||||||
className,
|
className,
|
||||||
isError,
|
isError,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -46,8 +88,20 @@ const RadioInput = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
}: RadioInputProps) => {
|
children,
|
||||||
|
}: RadioGroupProps) => {
|
||||||
|
const contextValue: RadioGroupContextValue = {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
color,
|
||||||
|
size,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RadioGroupContext.Provider value={contextValue}>
|
||||||
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
|
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
|
||||||
{/* Label atas */}
|
{/* Label atas */}
|
||||||
{label && (
|
{label && (
|
||||||
@@ -74,27 +128,18 @@ const RadioInput = ({
|
|||||||
className?.radioWrapper
|
className?.radioWrapper
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{options.map((option) => (
|
{/* Jika options diberikan, render otomatis */}
|
||||||
<label
|
{options &&
|
||||||
|
options.map((option) => (
|
||||||
|
<RadioGroupItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className={cn(
|
|
||||||
'flex flex-row items-center gap-2 cursor-pointer',
|
|
||||||
disabled && 'opacity-60 cursor-not-allowed'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type='radio'
|
|
||||||
name={name}
|
|
||||||
value={option.value}
|
value={option.value}
|
||||||
checked={value === option.value}
|
label={option.label}
|
||||||
onChange={onChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn('radio', variant, className?.radio)}
|
|
||||||
/>
|
/>
|
||||||
<span className='text-sm'>{option.label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Atau gunakan children untuk custom rendering */}
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Label bawah */}
|
{/* Label bawah */}
|
||||||
@@ -107,7 +152,61 @@ const RadioInput = ({
|
|||||||
<p className='text-sm text-error'>{errorMessage}</p>
|
<p className='text-sm text-error'>{errorMessage}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</RadioGroupContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RadioInput;
|
// RadioGroupItem Component
|
||||||
|
export interface RadioGroupItemProps {
|
||||||
|
value: string;
|
||||||
|
label?: string;
|
||||||
|
className?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
color?: RadioColor;
|
||||||
|
size?: RadioSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadioGroupItem = ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
disabled: itemDisabled,
|
||||||
|
color: itemColor,
|
||||||
|
size: itemSize,
|
||||||
|
}: RadioGroupItemProps) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value: groupValue,
|
||||||
|
color: groupColor,
|
||||||
|
size: groupSize,
|
||||||
|
disabled: groupDisabled,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
} = useRadioGroup();
|
||||||
|
|
||||||
|
const isDisabled = itemDisabled ?? groupDisabled;
|
||||||
|
const radioColor = itemColor ?? groupColor;
|
||||||
|
const radioSize = itemSize ?? groupSize;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={cn(
|
||||||
|
'flex flex-row items-center gap-2 cursor-pointer',
|
||||||
|
isDisabled && 'opacity-60 cursor-not-allowed',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
checked={groupValue === value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
disabled={isDisabled}
|
||||||
|
className={cn('radio', `radio-${radioColor}`, `radio-${radioSize}`)}
|
||||||
|
/>
|
||||||
|
{label && <span className='text-sm'>{label}</span>}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import { Size } from '@/types/theme';
|
||||||
|
|
||||||
interface MenuProps {
|
interface MenuProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
size?: Size;
|
||||||
|
direction?: 'vertical' | 'horizontal';
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Menu = ({ children, className }: MenuProps) => {
|
const Menu = ({
|
||||||
return (
|
children,
|
||||||
<ul className={cn('menu w-full p-0 gap-0.5', className)}>{children}</ul>
|
size = 'md',
|
||||||
);
|
direction = 'vertical',
|
||||||
|
className,
|
||||||
|
}: MenuProps) => {
|
||||||
|
const menuBaseClassName = cn('menu w-full', {
|
||||||
|
'menu-xs': size === 'xs',
|
||||||
|
'menu-sm': size === 'sm',
|
||||||
|
'menu-md': size === 'md',
|
||||||
|
'menu-lg': size === 'lg',
|
||||||
|
'menu-xl': size === 'xl',
|
||||||
|
'menu-vertical': direction === 'vertical',
|
||||||
|
'menu-horizontal': direction === 'horizontal',
|
||||||
|
});
|
||||||
|
|
||||||
|
return <ul className={cn(menuBaseClassName, className)}>{children}</ul>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Menu;
|
export default Menu;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface MenuItemProps {
|
|||||||
href?: string;
|
href?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -17,6 +18,7 @@ const MenuItem = ({
|
|||||||
href,
|
href,
|
||||||
icon,
|
icon,
|
||||||
active = false,
|
active = false,
|
||||||
|
isLoading = false,
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
}: MenuItemProps) => {
|
}: MenuItemProps) => {
|
||||||
@@ -50,17 +52,28 @@ const MenuItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
{href && (
|
{!isLoading && href && (
|
||||||
<Link href={href} className={menuItemBaseClassName}>
|
<Link href={href} className={menuItemBaseClassName}>
|
||||||
{menuItemContent}
|
{menuItemContent}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!href && (
|
{!isLoading && !href && (
|
||||||
<button className={menuItemBaseClassName} onClick={onClick}>
|
<button className={menuItemBaseClassName} onClick={onClick}>
|
||||||
{menuItemContent}
|
{menuItemContent}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<button className={menuItemBaseClassName}>
|
||||||
|
<span
|
||||||
|
className={cn('loading loading-dots loading-md mx-auto', {
|
||||||
|
'text-gray-400': !active,
|
||||||
|
'text-black': active,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import Button, { ButtonProps } from '@/components/Button';
|
|||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export type IconPosition = 'left' | 'center' | 'right';
|
||||||
|
|
||||||
export interface ConfirmationModalProps {
|
export interface ConfirmationModalProps {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
type?: 'info' | 'success' | 'error';
|
type?: 'info' | 'success' | 'error';
|
||||||
text?: string;
|
text?: string;
|
||||||
|
subtitleText?: string;
|
||||||
closeOnBackdrop?: boolean;
|
closeOnBackdrop?: boolean;
|
||||||
primaryButton?: ButtonProps & {
|
primaryButton?: ButtonProps & {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -24,17 +27,84 @@ export interface ConfirmationModalProps {
|
|||||||
modalBox?: string;
|
modalBox?: string;
|
||||||
};
|
};
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
iconSize?: number;
|
||||||
|
iconPosition?: IconPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iconConfig = {
|
||||||
|
info: {
|
||||||
|
icon: 'material-symbols:info-outline-rounded',
|
||||||
|
iconClassName: 'text-info-content',
|
||||||
|
bgClassName: 'bg-info',
|
||||||
|
outerRingClassName: 'bg-info/20',
|
||||||
|
borderClassName: 'border-info',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
icon: 'heroicons:check',
|
||||||
|
iconClassName: 'text-white',
|
||||||
|
bgClassName: 'bg-[#00D390]',
|
||||||
|
outerRingClassName: 'bg-[#00D3901F]',
|
||||||
|
borderClassName: 'border-[#CCF7EB]',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
icon: 'solar:danger-triangle-linear',
|
||||||
|
iconClassName: 'text-error-content',
|
||||||
|
bgClassName: 'bg-[#f03338]',
|
||||||
|
outerRingClassName: 'bg-[#f3cdcd]',
|
||||||
|
borderClassName: 'border-[#fff0ef]',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const ConfirmationModalIcon = ({
|
||||||
|
type,
|
||||||
|
size = 24,
|
||||||
|
}: {
|
||||||
|
type: 'info' | 'success' | 'error';
|
||||||
|
size?: number;
|
||||||
|
}) => {
|
||||||
|
const config = iconConfig[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center justify-center p-2'>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-full border-4 p-1',
|
||||||
|
config.outerRingClassName,
|
||||||
|
config.borderClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn('rounded-full p-1', config.outerRingClassName)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-full p-3 flex items-center justify-center',
|
||||||
|
config.bgClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={config.icon}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={config.iconClassName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ConfirmationModal = ({
|
const ConfirmationModal = ({
|
||||||
ref,
|
ref,
|
||||||
type = 'info',
|
type = 'info',
|
||||||
text,
|
text,
|
||||||
|
subtitleText,
|
||||||
closeOnBackdrop,
|
closeOnBackdrop,
|
||||||
primaryButton,
|
primaryButton,
|
||||||
secondaryButton,
|
secondaryButton,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
iconSize = 32,
|
||||||
|
iconPosition = 'center',
|
||||||
}: ConfirmationModalProps) => {
|
}: ConfirmationModalProps) => {
|
||||||
const [isPrimaryButtonLoading, setIsPrimaryButtonLoading] = useState(false);
|
const [isPrimaryButtonLoading, setIsPrimaryButtonLoading] = useState(false);
|
||||||
|
|
||||||
@@ -55,55 +125,52 @@ const ConfirmationModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal ref={ref} closeOnBackdrop={closeOnBackdrop} className={className}>
|
<Modal ref={ref} closeOnBackdrop={closeOnBackdrop} className={className}>
|
||||||
<div className='w-full flex flex-col gap-4'>
|
<div className='w-full flex flex-col gap-4'>
|
||||||
<div
|
{iconPosition === 'center' ? (
|
||||||
className={cn(
|
<>
|
||||||
'w-fit p-4 mx-auto flex flex-row justify-center items-center rounded-full',
|
<div className='w-fit mx-auto'>
|
||||||
{
|
<ConfirmationModalIcon type={type} size={iconSize} />
|
||||||
'bg-error': type === 'error',
|
|
||||||
'bg-info': type === 'info',
|
|
||||||
'bg-success': type === 'success',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{type === 'info' && (
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:info-outline-rounded'
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className='text-info-content'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type === 'success' && (
|
|
||||||
<Icon
|
|
||||||
icon='qlementine-icons:success-12'
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className='text-success-content'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type === 'error' && (
|
|
||||||
<Icon
|
|
||||||
icon='solar:danger-triangle-linear'
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className='text-error-content'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='text-center font-medium'>
|
<p className='text-center font-medium'>
|
||||||
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{subtitleText && (
|
||||||
|
<p className='text-center text-sm text-gray-400'>
|
||||||
|
{subtitleText}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-row items-center gap-4', {
|
||||||
|
'flex-row': iconPosition === 'left',
|
||||||
|
'flex-row-reverse': iconPosition === 'right',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className='w-fit'>
|
||||||
|
<ConfirmationModalIcon type={type} size={iconSize} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<p className='font-medium'>
|
||||||
|
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{subtitleText && (
|
||||||
|
<p className='text-sm text-gray-400'>{subtitleText}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{children && <div className='w-full'>{children}</div>}
|
{children && <div className='w-full'>{children}</div>}
|
||||||
|
|
||||||
<div className='w-full flex flex-row gap-2'>
|
<div className='w-full flex flex-row gap-2'>
|
||||||
{secondaryButton && secondaryButton.text && (
|
{secondaryButton && secondaryButton.text && (
|
||||||
<Button
|
<Button
|
||||||
{...secondaryButton}
|
{...secondaryButton}
|
||||||
variant='ghost'
|
variant='outline'
|
||||||
color={secondaryButton?.color}
|
color={secondaryButton?.color}
|
||||||
isLoading={secondaryButton?.isLoading}
|
isLoading={secondaryButton?.isLoading}
|
||||||
disabled={
|
disabled={
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import Menu from '@/components/menu/Menu';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { cn, isPathActive } from '@/lib/helper';
|
||||||
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
|
||||||
|
export interface SidebarMenuItem {
|
||||||
|
type?: 'item' | 'title';
|
||||||
|
text: string;
|
||||||
|
link: string;
|
||||||
|
icon?: string;
|
||||||
|
submenu?: SidebarMenuItem[];
|
||||||
|
permission?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarMenuItemProps {
|
||||||
|
item: SidebarMenuItem;
|
||||||
|
activeLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarMenuProps {
|
||||||
|
menu: SidebarMenuItem[];
|
||||||
|
activeLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
|
||||||
|
const { permissionCheck } = useAuth();
|
||||||
|
const isItemActive = isPathActive(activeLink, item.link);
|
||||||
|
|
||||||
|
const isUserPermitted = item.permission
|
||||||
|
? item.permission?.some((permissionName) => permissionCheck(permissionName))
|
||||||
|
: true;
|
||||||
|
|
||||||
|
if (!isUserPermitted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItemWithoutSubmenu = (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href={item.link}
|
||||||
|
className={cn(
|
||||||
|
{
|
||||||
|
'menu-active border-2 border-solid border-base-300': isItemActive,
|
||||||
|
},
|
||||||
|
'px-3 py-1.5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.icon && <Icon icon={item.icon} width={20} height={20} />}
|
||||||
|
|
||||||
|
<span className='text-base'>{item.text}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!item.submenu || item.submenu.length === 0) {
|
||||||
|
return menuItemWithoutSubmenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItemWithSubmenu = (
|
||||||
|
<li>
|
||||||
|
<details open={isItemActive}>
|
||||||
|
<summary
|
||||||
|
className={cn({
|
||||||
|
'text-primary': isItemActive,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.icon && <Icon icon={item.icon} width={20} height={20} />}
|
||||||
|
|
||||||
|
<span className='text-base'>{item.text}</span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{item.submenu.map((submenuItem, submenuIdx) => (
|
||||||
|
<SidebarMenuItem
|
||||||
|
key={`submenu#${submenuIdx}`}
|
||||||
|
item={submenuItem}
|
||||||
|
activeLink={activeLink}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
return menuItemWithSubmenu;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
{menu.map((menuItem, menuIdx) => {
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem
|
||||||
|
key={menuIdx}
|
||||||
|
item={menuItem}
|
||||||
|
activeLink={activeLink}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SidebarMenu;
|
||||||
@@ -144,33 +144,45 @@ const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => {
|
|||||||
|
|
||||||
export const formatGroupedApprovalsToApprovalSteps = (
|
export const formatGroupedApprovalsToApprovalSteps = (
|
||||||
approvalLine: ApprovalLine,
|
approvalLine: ApprovalLine,
|
||||||
groupedApprovals: BaseGroupedApproval[],
|
groupedApprovals: BaseGroupedApproval[] | undefined,
|
||||||
latestApproval: BaseApproval
|
latestApproval: BaseApproval | undefined
|
||||||
): ApprovalStepsProps['approvals'] => {
|
): ApprovalStepsProps['approvals'] => {
|
||||||
const formattedApprovalSteps: ApprovalStepsProps['approvals'] =
|
const formattedApprovalSteps: ApprovalStepsProps['approvals'] =
|
||||||
approvalLine.map((approvalLineItem) => {
|
approvalLine.map((approvalLineItem) => {
|
||||||
const approvalGroup = groupedApprovals.find(
|
const approvalGroup = groupedApprovals?.find(
|
||||||
(approvalGroupItem) =>
|
(approvalGroupItem) =>
|
||||||
approvalGroupItem.step_number === approvalLineItem.step_number
|
approvalGroupItem.step_number === approvalLineItem.step_number
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentStepNumber = approvalLineItem.step_number;
|
const currentStepNumber = approvalLineItem.step_number;
|
||||||
const lastStepNumber =
|
const lastStepNumber =
|
||||||
groupedApprovals[groupedApprovals.length - 1]?.step_number;
|
groupedApprovals?.[groupedApprovals.length - 1]?.step_number;
|
||||||
|
|
||||||
const isLatestApprovalRejected = latestApproval.action === 'REJECTED';
|
const isLatestApprovalRejected = latestApproval?.action === 'REJECTED';
|
||||||
|
|
||||||
if (!approvalGroup && currentStepNumber <= lastStepNumber) {
|
// Only throw error if we have a valid lastStepNumber to compare against
|
||||||
throw new Error(
|
if (
|
||||||
`Approval dengan ${approvalLineItem.step_name} tidak ditemukan!`
|
!approvalGroup &&
|
||||||
);
|
lastStepNumber !== undefined &&
|
||||||
|
currentStepNumber <= lastStepNumber
|
||||||
|
) {
|
||||||
|
// throw new Error(
|
||||||
|
// `Approval dengan ${approvalLineItem.step_name} tidak ditemukan!`
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!approvalGroup) {
|
if (!approvalGroup) {
|
||||||
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
|
// Check if this step is waiting (only if we have latestApproval)
|
||||||
|
const isWaiting =
|
||||||
|
latestApproval?.step_number !== undefined &&
|
||||||
|
currentStepNumber === latestApproval.step_number + 1;
|
||||||
|
|
||||||
|
// Check if previous approval was rejected
|
||||||
const isPreviousApprovalRejected =
|
const isPreviousApprovalRejected =
|
||||||
groupedApprovals[groupedApprovals.length - 1].approvals[0].action ===
|
groupedApprovals &&
|
||||||
'REJECTED';
|
groupedApprovals.length > 0 &&
|
||||||
|
groupedApprovals[groupedApprovals.length - 1]?.approvals?.[0]
|
||||||
|
?.action === 'REJECTED';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: approvalLineItem.step_name,
|
name: approvalLineItem.step_name,
|
||||||
@@ -184,7 +196,11 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
|||||||
|
|
||||||
let approvalStatus: ApprovalStepStatus = 'IDLE';
|
let approvalStatus: ApprovalStepStatus = 'IDLE';
|
||||||
|
|
||||||
if (approvalGroup.step_number <= latestApproval.step_number) {
|
// Only compare if latestApproval and its step_number exist
|
||||||
|
if (
|
||||||
|
latestApproval?.step_number !== undefined &&
|
||||||
|
approvalGroup.step_number <= latestApproval.step_number
|
||||||
|
) {
|
||||||
if (approvalGroup.approvals) {
|
if (approvalGroup.approvals) {
|
||||||
switch (approvalGroup?.approvals[0]?.action) {
|
switch (approvalGroup?.approvals[0]?.action) {
|
||||||
case 'CREATED':
|
case 'CREATED':
|
||||||
@@ -203,6 +219,7 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
latestApproval?.step_number !== undefined &&
|
||||||
approvalGroup.step_number === latestApproval.step_number + 1 &&
|
approvalGroup.step_number === latestApproval.step_number + 1 &&
|
||||||
!isLatestApprovalRejected
|
!isLatestApprovalRejected
|
||||||
) {
|
) {
|
||||||
@@ -292,7 +309,7 @@ const useApprovalSteps = ({
|
|||||||
moduleId: string;
|
moduleId: string;
|
||||||
params?: {
|
params?: {
|
||||||
page?: number;
|
page?: number;
|
||||||
limit: number;
|
limit: number | string;
|
||||||
search?: string;
|
search?: string;
|
||||||
group_step_number?: boolean;
|
group_step_number?: boolean;
|
||||||
};
|
};
|
||||||
@@ -353,14 +370,33 @@ const useApprovalSteps = ({
|
|||||||
|
|
||||||
// Formatting Akhir
|
// Formatting Akhir
|
||||||
const approvals = useMemo(() => {
|
const approvals = useMemo(() => {
|
||||||
if (isLoading || !approvalLines.length || !latestApproval) {
|
if (isLoading || !approvalLines.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to derive latestApproval from groupedApprovals if not provided
|
||||||
|
let effectiveLatestApproval = latestApproval;
|
||||||
|
|
||||||
|
if (!effectiveLatestApproval && groupedApprovals.length > 0) {
|
||||||
|
// Get all approvals from grouped data
|
||||||
|
const allApprovals = groupedApprovals.flatMap((group) => group.approvals);
|
||||||
|
|
||||||
|
if (allApprovals.length > 0) {
|
||||||
|
// Use the most recent approval (last in array)
|
||||||
|
effectiveLatestApproval = allApprovals[allApprovals.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no latestApproval, return empty
|
||||||
|
if (!effectiveLatestApproval) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return formatGroupedApprovalsToApprovalSteps(
|
return formatGroupedApprovalsToApprovalSteps(
|
||||||
approvalLines,
|
approvalLines,
|
||||||
groupedApprovals,
|
groupedApprovals,
|
||||||
latestApproval
|
effectiveLatestApproval
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Gagal memformat approval steps:', error);
|
console.warn('Gagal memformat approval steps:', error);
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Tabs from '@/components/Tabs';
|
||||||
|
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
||||||
|
import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent';
|
||||||
|
import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ClosingGeneralInformation,
|
||||||
|
BaseClosingSales,
|
||||||
|
ClosingHppExpedition,
|
||||||
|
} from '@/types/api/closing';
|
||||||
|
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
||||||
|
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
|
||||||
|
import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
|
||||||
|
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
|
||||||
|
import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable';
|
||||||
|
import ClosingKandangList from '@/components/pages/closing/ClosingKandangList';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
|
interface ClosingDetailProps {
|
||||||
|
id: number;
|
||||||
|
initialValue?: ClosingGeneralInformation;
|
||||||
|
salesData?: BaseClosingSales;
|
||||||
|
hppExpeditionData?: ClosingHppExpedition;
|
||||||
|
projectData?: ProjectFlock;
|
||||||
|
kandangData?: ProjectFlockKandang;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||||
|
id,
|
||||||
|
initialValue,
|
||||||
|
salesData,
|
||||||
|
hppExpeditionData,
|
||||||
|
projectData,
|
||||||
|
kandangData,
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<string>('sapronak');
|
||||||
|
|
||||||
|
const closingDetailTabs = useMemo(() => {
|
||||||
|
const validTabs = [
|
||||||
|
{
|
||||||
|
id: 'sapronak',
|
||||||
|
label: 'Sapronak',
|
||||||
|
content: <ClosingSapronakTabContent projectFlockId={id} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perhitunganSapronak',
|
||||||
|
label: 'Perhitungan Sapronak',
|
||||||
|
content: (
|
||||||
|
<ClosingSapronakCalculationTabContent
|
||||||
|
closingGeneralInformation={initialValue}
|
||||||
|
projectFlockId={id}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'penjualan',
|
||||||
|
label: 'Penjualan',
|
||||||
|
content: <SalesReportTable initialValues={salesData} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'overhead',
|
||||||
|
label: 'Overhead',
|
||||||
|
content: <ClosingOverheadTabContent projectFlockId={id} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hppEkspedisi',
|
||||||
|
label: 'HPP Ekspedisi',
|
||||||
|
content: <HppExpeditionReportTable initialValues={hppExpeditionData} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dataProduksi',
|
||||||
|
label: 'Data Produksi',
|
||||||
|
content: <ClosingProductionDataTabContent projectFlockId={id} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'keuangan',
|
||||||
|
label: 'Keuangan',
|
||||||
|
content: <ClosingFinanceTabContent projectFlockId={id} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return validTabs;
|
||||||
|
}, [initialValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full max-w-7xl pb-16'>
|
||||||
|
<header className='flex flex-col gap-4'>
|
||||||
|
<Button
|
||||||
|
href={
|
||||||
|
!kandangData ? '/closing' : `/closing/detail/?closingId=${id}`
|
||||||
|
}
|
||||||
|
variant='link'
|
||||||
|
className='w-fit p-0 text-primary'
|
||||||
|
>
|
||||||
|
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<h1 className='text-2xl font-bold text-center'>Detail Closing</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ClosingGeneralInformationTable
|
||||||
|
initialValue={initialValue}
|
||||||
|
projectData={projectData}
|
||||||
|
kandangData={kandangData}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!kandangData && (
|
||||||
|
<ClosingKandangList
|
||||||
|
initialValue={initialValue}
|
||||||
|
projectData={projectData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
activeTabId={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
tabs={closingDetailTabs}
|
||||||
|
variant='lifted'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full mt-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingDetail;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import ClosingFinanceTable from '@/components/pages/closing/ClosingFinanceTable';
|
||||||
|
|
||||||
|
const ClosingFinanceTabContent = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: {
|
||||||
|
projectFlockId: number;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
{projectFlockId && (
|
||||||
|
<ClosingFinanceTable projectFlockId={projectFlockId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingFinanceTabContent;
|
||||||
@@ -0,0 +1,595 @@
|
|||||||
|
import Card from '@/components/Card';
|
||||||
|
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import {
|
||||||
|
DataSummarySubTotal,
|
||||||
|
HppPurchaseData,
|
||||||
|
ProfitLossDataAmount,
|
||||||
|
} from '@/types/api/closing';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
type HppTableRow =
|
||||||
|
| (HppPurchaseData & {
|
||||||
|
group_name: string;
|
||||||
|
group_index: number;
|
||||||
|
isGroupHeader?: boolean;
|
||||||
|
})
|
||||||
|
| {
|
||||||
|
group_name: string;
|
||||||
|
group_index: number;
|
||||||
|
isGroupHeader: true;
|
||||||
|
type?: never;
|
||||||
|
budgeting?: never;
|
||||||
|
realization?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: string;
|
||||||
|
group_name: string;
|
||||||
|
group_index: number;
|
||||||
|
isGroupHeader: false;
|
||||||
|
budgeting?: { rp_per_bird: number; rp_per_kg: number; amount: number };
|
||||||
|
realization?: { rp_per_bird: number; rp_per_kg: number; amount: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProfitLossTableRow =
|
||||||
|
| (DataSummarySubTotal & {
|
||||||
|
type: string;
|
||||||
|
group_name: string;
|
||||||
|
group_index: number;
|
||||||
|
isGroupHeader?: boolean;
|
||||||
|
})
|
||||||
|
| {
|
||||||
|
group_name: string;
|
||||||
|
group_index: number;
|
||||||
|
isGroupHeader: true;
|
||||||
|
type?: never;
|
||||||
|
rp_per_bird?: never;
|
||||||
|
rp_per_kg?: never;
|
||||||
|
amount?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClosingFinanceTable = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: {
|
||||||
|
projectFlockId: number;
|
||||||
|
}) => {
|
||||||
|
const { data: finance, isLoading } = useSWR(
|
||||||
|
`/closing/finance/${projectFlockId}`,
|
||||||
|
() => ClosingApi.getFinance(projectFlockId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticHppRows: Array<{
|
||||||
|
group_name: string;
|
||||||
|
type: string;
|
||||||
|
group_index: number;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
group_name: 'HPP dan Pengeluaran',
|
||||||
|
type: 'Pembelian PAKAN',
|
||||||
|
group_index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group_name: 'HPP dan Pengeluaran',
|
||||||
|
type: 'Pembelian STARTER',
|
||||||
|
group_index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group_name: 'HPP dan Pengeluaran',
|
||||||
|
type: 'Pembelian DOC',
|
||||||
|
group_index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group_name: 'HPP dan Pengeluaran',
|
||||||
|
type: 'Pembelian PULLET',
|
||||||
|
group_index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group_name: 'HPP dan Pengeluaran',
|
||||||
|
type: 'Pembelian LAYER',
|
||||||
|
group_index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group_name: 'HPP dan Bahan Baku',
|
||||||
|
type: 'Pengeluaran Overhead',
|
||||||
|
group_index: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group_name: 'HPP dan Bahan Baku',
|
||||||
|
type: 'Beban Ekspedisi',
|
||||||
|
group_index: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const hppTableData: HppTableRow[] = [
|
||||||
|
{
|
||||||
|
group_name: 'HPP dan Pengeluaran',
|
||||||
|
group_index: 0,
|
||||||
|
isGroupHeader: true as const,
|
||||||
|
},
|
||||||
|
...staticHppRows
|
||||||
|
.filter((row) => row.group_index === 0)
|
||||||
|
.map((staticRow) => {
|
||||||
|
const apiData = isResponseSuccess(finance)
|
||||||
|
? finance.data.hpp_purchases.hpp
|
||||||
|
.find((g) => g.group_name === staticRow.group_name)
|
||||||
|
?.data.find((d) => d.type === staticRow.type)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
group_name: staticRow.group_name,
|
||||||
|
group_index: staticRow.group_index,
|
||||||
|
type: staticRow.type,
|
||||||
|
budgeting: apiData?.budgeting || {
|
||||||
|
rp_per_bird: 0,
|
||||||
|
rp_per_kg: 0,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
realization: apiData?.realization || {
|
||||||
|
rp_per_bird: 0,
|
||||||
|
rp_per_kg: 0,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
isGroupHeader: false as const,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
group_name: 'HPP dan Bahan Baku',
|
||||||
|
group_index: 1,
|
||||||
|
isGroupHeader: true as const,
|
||||||
|
},
|
||||||
|
...staticHppRows
|
||||||
|
.filter((row) => row.group_index === 1)
|
||||||
|
.map((staticRow) => {
|
||||||
|
const apiData = isResponseSuccess(finance)
|
||||||
|
? finance.data.hpp_purchases.hpp
|
||||||
|
.find((g) => g.group_name === staticRow.group_name)
|
||||||
|
?.data.find((d) => d.type === staticRow.type)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
group_name: staticRow.group_name,
|
||||||
|
group_index: staticRow.group_index,
|
||||||
|
type: staticRow.type,
|
||||||
|
budgeting: apiData?.budgeting || {
|
||||||
|
rp_per_bird: 0,
|
||||||
|
rp_per_kg: 0,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
realization: apiData?.realization || {
|
||||||
|
rp_per_bird: 0,
|
||||||
|
rp_per_kg: 0,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
isGroupHeader: false as const,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
group_name: 'HPP',
|
||||||
|
group_index: 2,
|
||||||
|
isGroupHeader: true as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance)
|
||||||
|
? [
|
||||||
|
// Pembelian group
|
||||||
|
...finance.data.profit_loss.data.pembelian.map((item) => ({
|
||||||
|
label: 'Pembelian',
|
||||||
|
group_name: 'Pembelian',
|
||||||
|
group_index: 1,
|
||||||
|
type: item.type,
|
||||||
|
rp_per_bird: item.rp_per_bird,
|
||||||
|
rp_per_kg: item.rp_per_kg,
|
||||||
|
amount: item.amount,
|
||||||
|
isGroupHeader: false as const,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
label: finance.data.profit_loss.data.summary.gross_profit.label,
|
||||||
|
group_name: 'Penjualan',
|
||||||
|
group_index: 0,
|
||||||
|
isGroupHeader: true as const,
|
||||||
|
type: finance.data.profit_loss.data.summary.gross_profit.label,
|
||||||
|
rp_per_bird:
|
||||||
|
finance.data.profit_loss.data.summary.gross_profit.rp_per_bird,
|
||||||
|
rp_per_kg:
|
||||||
|
finance.data.profit_loss.data.summary.gross_profit.rp_per_kg,
|
||||||
|
amount: finance.data.profit_loss.data.summary.gross_profit.amount,
|
||||||
|
},
|
||||||
|
// Penjualan group
|
||||||
|
...finance.data.profit_loss.data.penjualan.map((item) => ({
|
||||||
|
label: 'Penjualan',
|
||||||
|
group_name: 'Penjualan',
|
||||||
|
group_index: 0,
|
||||||
|
type: item.type,
|
||||||
|
rp_per_bird: item.rp_per_bird,
|
||||||
|
rp_per_kg: item.rp_per_kg,
|
||||||
|
amount: item.amount,
|
||||||
|
isGroupHeader: false as const,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
label: finance.data.profit_loss.data.summary.sub_total.label,
|
||||||
|
group_name: 'Pembelian',
|
||||||
|
group_index: 1,
|
||||||
|
isGroupHeader: true as const,
|
||||||
|
type: finance.data.profit_loss.data.summary.sub_total.label,
|
||||||
|
rp_per_bird:
|
||||||
|
finance.data.profit_loss.data.summary.sub_total.rp_per_bird,
|
||||||
|
rp_per_kg: finance.data.profit_loss.data.summary.sub_total.rp_per_kg,
|
||||||
|
amount: finance.data.profit_loss.data.summary.sub_total.amount,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
variant='bordered'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-2 gap-6'>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<div>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatTitleCase(
|
||||||
|
finance.data.profit_loss.data.summary.gross_profit
|
||||||
|
.label || '-'
|
||||||
|
)
|
||||||
|
: 'Laba Rugi Brutto'}
|
||||||
|
</div>
|
||||||
|
<div className='text-lg font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.data.summary.gross_profit.amount
|
||||||
|
)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<div>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatTitleCase(
|
||||||
|
finance.data.profit_loss.data.summary.net_profit.label ||
|
||||||
|
'-'
|
||||||
|
)
|
||||||
|
: 'Laba Rugi Netto'}
|
||||||
|
</div>
|
||||||
|
<div className='text-lg font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.data.summary.net_profit.amount
|
||||||
|
)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? finance.data.hpp_purchases.title
|
||||||
|
: 'HPP Purchases'
|
||||||
|
}
|
||||||
|
variant='bordered'
|
||||||
|
collapsible
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mt-6 p-0 mb-0'>
|
||||||
|
<Table<HppTableRow>
|
||||||
|
data={hppTableData}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'No.',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item, index) => {
|
||||||
|
if (item.isGroupHeader) return '-';
|
||||||
|
const dataRowsBefore = hppTableData
|
||||||
|
.slice(0, index)
|
||||||
|
.filter((row) => !row.isGroupHeader).length;
|
||||||
|
return dataRowsBefore + 1;
|
||||||
|
},
|
||||||
|
footer: (props) => {
|
||||||
|
return 'HPP';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Type',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => formatTitleCase(item.type || '-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Budgeting',
|
||||||
|
enableSorting: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
header: 'Rp/Ekor',
|
||||||
|
id: 'budgeting_rp_per_bird',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.budgeting?.rp_per_bird || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'budgeting_rp_per_bird' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp_purchases.summary_hpp?.budgeting
|
||||||
|
?.rp_per_bird || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Rp/Kg',
|
||||||
|
id: 'budgeting_rp_per_kg',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.budgeting?.rp_per_kg || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'budgeting_rp_per_kg' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp_purchases.summary_hpp?.budgeting
|
||||||
|
?.rp_per_kg || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jumlah (Rp)',
|
||||||
|
id: 'budgeting_amount',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.budgeting?.amount || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'budgeting_amount' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp_purchases.summary_hpp?.budgeting
|
||||||
|
?.amount || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Realization',
|
||||||
|
enableSorting: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
header: 'Rp/Ekor',
|
||||||
|
id: 'realization_rp_per_bird',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.realization?.rp_per_bird || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'realization_rp_per_bird' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp_purchases.summary_hpp
|
||||||
|
?.realization?.rp_per_bird || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Rp/Kg',
|
||||||
|
id: 'realization_rp_per_kg',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.realization?.rp_per_kg || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'realization_rp_per_kg' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp_purchases.summary_hpp
|
||||||
|
?.realization?.rp_per_kg || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jumlah (Rp)',
|
||||||
|
id: 'realization_amount',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.realization?.amount || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'realization_amount' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp_purchases.summary_hpp
|
||||||
|
?.realization?.amount || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderCustomRow={(row) => {
|
||||||
|
const rowData = row.original;
|
||||||
|
if (rowData.isGroupHeader) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
|
></td>
|
||||||
|
<td
|
||||||
|
colSpan={7}
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
|
>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatTitleCase(rowData.group_name ?? '-')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
renderFooter={isResponseSuccess(finance)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? finance.data.profit_loss.title
|
||||||
|
: 'Profit/Loss'
|
||||||
|
}
|
||||||
|
variant='bordered'
|
||||||
|
collapsible
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mt-6 p-0 mb-0'>
|
||||||
|
<Table<ProfitLossTableRow>
|
||||||
|
data={profitLossTableData}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'Jenis',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => item.type,
|
||||||
|
cell: (item) => (
|
||||||
|
<div className=''>
|
||||||
|
{formatTitleCase(item.row.original.type || '-')}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
footer: (item) => (
|
||||||
|
<div className='font-bold uppercase'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatTitleCase(
|
||||||
|
finance.data.profit_loss.data.summary.net_profit
|
||||||
|
.label || '-'
|
||||||
|
)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Rp/Ekor',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
||||||
|
footer: (item) => (
|
||||||
|
<div className='font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.data.summary.net_profit
|
||||||
|
.rp_per_bird || 0
|
||||||
|
)
|
||||||
|
: formatCurrency(0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Rp/Kg',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
||||||
|
footer: (item) => (
|
||||||
|
<div className='font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.data.summary.net_profit
|
||||||
|
.rp_per_kg || 0
|
||||||
|
)
|
||||||
|
: formatCurrency(0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jumlah (Rp)',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => formatCurrency(item.amount || 0),
|
||||||
|
footer: (item) => (
|
||||||
|
<div className='font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.data.summary.net_profit
|
||||||
|
.amount || 0
|
||||||
|
)
|
||||||
|
: formatCurrency(0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderCustomRow={(row) => {
|
||||||
|
const rowData = row.original;
|
||||||
|
if (rowData.isGroupHeader) {
|
||||||
|
if (rowData.amount) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
|
>
|
||||||
|
<div className='font-bold ps-6 uppercase'>
|
||||||
|
{formatTitleCase(rowData.label ?? '-')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
|
>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
|
>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
|
>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.amount ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
colSpan={4}
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
|
>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatTitleCase(rowData.group_name ?? '-')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
className={{
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
}}
|
||||||
|
renderFooter={isResponseSuccess(finance)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingFinanceTable;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user