mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Compare commits
814 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bac3f30ce3 | |||
| be725d42c3 | |||
| b37c3f87b0 | |||
| ae4c17b391 | |||
| 48dd6d7218 | |||
| cee3d4ba90 | |||
| a8d7fdc30d | |||
| 2bb2da74e6 | |||
| fd024fdb8f | |||
| 79a89ea193 | |||
| 611655e408 | |||
| 702943c55c | |||
| 58532881f4 | |||
| 4073f4dfde | |||
| 94e2d71dba | |||
| 944f479e2d | |||
| 5046d687b5 | |||
| 711deda6a8 | |||
| 029be31020 | |||
| ed7563a028 | |||
| f82ca4f959 | |||
| 0cc01ae738 | |||
| 1de743a404 | |||
| 68c1e76a4a | |||
| 2001cdb843 | |||
| b8590040ff | |||
| 909aa3357c | |||
| 507543eff8 | |||
| 8dc6b3d1db | |||
| 22ce1b1142 | |||
| e126ab4a0e | |||
| 7685be0c7b | |||
| 8b72f58467 | |||
| 87c25a8bc4 | |||
| 1e4c826a0a | |||
| 6d3632a385 | |||
| d75ac635df | |||
| 352fd701cb | |||
| 2a97f9d504 | |||
| b805fb4ae1 | |||
| 642f966985 | |||
| c3e4d4c630 | |||
| b616f28c95 | |||
| 334202569c | |||
| 1a1fefc237 | |||
| 47690f82ac | |||
| b868a37485 | |||
| 034d185b84 | |||
| e4a6b22357 | |||
| 82eac4a965 | |||
| 20c3e2d6b4 | |||
| f24ae992e6 | |||
| 4f375a4f0b | |||
| 510d10270e | |||
| b083b9cb1a | |||
| 93d14cb98b | |||
| b0bd2bd8a5 | |||
| c0bba827a0 | |||
| 032e9d45b3 | |||
| 4027b25598 | |||
| 70b1ba3f6b | |||
| d34c113be3 | |||
| 6a08854603 | |||
| 824eed910a | |||
| d769bfe452 | |||
| 274322606d | |||
| 62c3d2af53 | |||
| 01b9595606 | |||
| 09065f59cf | |||
| ad79f29494 | |||
| a26665e4ac | |||
| eaaed9521b | |||
| 5bb94b5679 | |||
| 3c7f630580 | |||
| 6ce5a5b625 | |||
| c12a58cb6d | |||
| 34c1da94d8 | |||
| 5b84a19afa | |||
| a4a2b76277 | |||
| 23abdbb78f | |||
| 6a39c2fd3f | |||
| f9215738aa | |||
| b12a1ebd36 | |||
| 0fefe5e035 | |||
| ef95d1a0e8 | |||
| 30ab69ae21 | |||
| 5b28067203 | |||
| ffea96edf9 | |||
| 1a74a9d33f | |||
| b9990e0253 | |||
| 95a7afdaa6 | |||
| b198f24b75 | |||
| b7c3b9313c | |||
| c74733430b | |||
| f6fe2d4eb1 | |||
| d624da97c3 | |||
| fdf680bc38 | |||
| e9d9897e1d | |||
| 70521330e4 | |||
| 63e5962a4e | |||
| 835074f538 | |||
| e69e5da2c3 | |||
| e451a128c5 | |||
| 23ab4b15e1 | |||
| d523a01e34 | |||
| e00b7bc3f2 | |||
| 51f157dfad | |||
| c1d71ee3c6 | |||
| c8b3e52ac0 | |||
| b2ef545f63 | |||
| a6d187a8b3 | |||
| 24e2bcf35d | |||
| 417d08e0fc | |||
| 6e9d065bc6 | |||
| af939ee225 | |||
| 391b355e8d | |||
| 4ddc44b59c | |||
| e0b4805d0a | |||
| 074e6fad05 | |||
| e640bce8ea | |||
| f1e5692f8f | |||
| 655e971788 | |||
| 00e18d6d0d | |||
| 343cc7c4e7 | |||
| 4e8b17f55c | |||
| 862e056950 | |||
| 1310c7401c | |||
| d0f2fefe1c | |||
| 6cb517ac92 | |||
| c698893f88 | |||
| cb236c191b | |||
| ac764c9d3b | |||
| b33e7a1919 | |||
| 28a5343592 | |||
| d3c3d9c9c6 | |||
| 42253d123b | |||
| 539b329b6f | |||
| 427887a0e0 | |||
| 7e58e46254 | |||
| c876824c8f | |||
| 9c69369a51 | |||
| 7b28e47c68 | |||
| 432ea1e975 | |||
| 14f216a352 | |||
| 8bda56e5d3 | |||
| 429f2b9109 | |||
| 9d6455167f | |||
| e3274a3353 | |||
| 07fd71558e | |||
| d2a69917e7 | |||
| af5dfa9292 | |||
| 8662bcb63b | |||
| f68e59e8c7 | |||
| b520b4ee54 | |||
| 89d9d40713 | |||
| 17378d8408 | |||
| 25544e2e38 | |||
| 89b54f6f87 | |||
| 3a431352ed | |||
| f6afb741af | |||
| f4bb87550c | |||
| 3d468d9507 | |||
| 0c79e86736 | |||
| 1b90d657ff | |||
| 0d025ba34c | |||
| 8c3cd3bc53 | |||
| 75e7b9a6de | |||
| 00c432a918 | |||
| f6cf22f885 | |||
| 3d86c9ce6b | |||
| d93f0c26b6 | |||
| e8dd4f3759 | |||
| edd59598f9 | |||
| 964a4500ab | |||
| 9a650a130d | |||
| 2f2c1fca07 | |||
| 2680d5a24d | |||
| 0f9019e7b4 | |||
| 14b7f06369 | |||
| 4dd50622a9 | |||
| 6022ff2dae | |||
| 9164b985b1 | |||
| 38cab1464c | |||
| 71b7598f87 | |||
| 075e5e452f | |||
| 8cd054e6aa | |||
| a9bdb6c36e | |||
| 8b02d0df1c | |||
| 470cdb8b02 | |||
| da40e7d7be | |||
| 9260f1aff6 | |||
| 0087ba384c | |||
| 71a41d3f37 | |||
| c58dde960c | |||
| 4e88e76538 | |||
| e6ac11893a | |||
| 83f1ba46a7 | |||
| fc76b44279 | |||
| dbe6ced602 | |||
| f01e764d9c | |||
| 6467af35bc | |||
| ac227f7780 | |||
| c8f1ea0e4f | |||
| 6067c00219 | |||
| 283c2b2a44 | |||
| 7ec4105454 | |||
| a151abfbe9 | |||
| e14d10c503 | |||
| d0ba9eadbd | |||
| 2190f65cb2 | |||
| b82ba60a32 | |||
| 30ed70b669 | |||
| 69a8899cac | |||
| 9f41768e54 | |||
| c951f09667 | |||
| d3c4706d87 | |||
| 64605b168e | |||
| e421c7d422 | |||
| 4bd6ac3cac | |||
| e638856ea9 | |||
| c45c8601cb | |||
| 57a867f611 | |||
| a5758aece4 | |||
| 3fdb10ec7f | |||
| 1ee0454e6b | |||
| 0c4c0ce3ab | |||
| 00e0202be2 | |||
| 3d49947c1e | |||
| 1ab72b8637 | |||
| f98a597115 | |||
| 1be61ae4ff | |||
| 1b64c1f5d1 | |||
| 7485919e52 | |||
| e5d9612e29 | |||
| b6ac8026c7 | |||
| d9ebde65cb | |||
| c6f881c78d | |||
| 18b036285a | |||
| 5648b51c2e | |||
| c7bad200ae | |||
| a2dd781140 | |||
| 10976452f5 | |||
| 6ffb6e1560 | |||
| 138c97a695 | |||
| 56f57c4a6b | |||
| c2479ad248 | |||
| b3f4e42f1a | |||
| ac8c39324b | |||
| 7478c2597f | |||
| 5975340c3d | |||
| e5318fd6b5 | |||
| def7ee4a0b | |||
| 26811f5e3e | |||
| 4485ea8181 | |||
| 57ca050100 | |||
| b64ab6567b | |||
| 478ca186d3 | |||
| 47262adaf1 | |||
| e2249cf73a | |||
| 0e7c178736 | |||
| 4a974048a7 | |||
| cd8ab8844b | |||
| 5d88af1a31 | |||
| fa3ba46810 | |||
| 4215c6c6ce | |||
| 6670f1e31b | |||
| b2f4317c08 | |||
| b9fb3c8311 | |||
| 305ad67cb4 | |||
| e3ecf5dc50 | |||
| bc8ba1df9c | |||
| e7d2c3bc13 | |||
| b7a055888b | |||
| 8d09aec66a | |||
| 776b809931 | |||
| c6fb707a9f | |||
| 569a8b495b | |||
| 1ff1e53e02 | |||
| 8e3282bb7d | |||
| 3c0bd647a8 | |||
| 557e20cffe | |||
| 5124c1b66a | |||
| c9092f36e3 | |||
| 73ab5703db | |||
| 2959295bfa | |||
| 03b16248e5 | |||
| f264474293 | |||
| 963377199f | |||
| 4bbf6fd7f8 | |||
| 4422b7391a | |||
| c770651a01 | |||
| 603f95a9b2 | |||
| f26e54e8f2 | |||
| bc53b9073c | |||
| 45f12cad4f | |||
| 5dccaf40cb | |||
| fde9c449a6 | |||
| ecb497430a | |||
| 8c17367fb6 | |||
| 21ac73527d | |||
| f00e772018 | |||
| f7b2e3c6f2 | |||
| 5fc01a9afa | |||
| 3ed3e2e21a | |||
| 7d1992d075 | |||
| f63d3d3870 | |||
| 63dac00f17 | |||
| efcc14f3ab | |||
| 5e64d37c61 | |||
| c7022ee200 | |||
| 3ac0672f7e | |||
| 9f4f140018 | |||
| e0c347c3d5 | |||
| 13d57c206b | |||
| 773aa2dbb1 | |||
| f14adc46d3 | |||
| e7592eb221 | |||
| 32f202d814 | |||
| 942b19375e | |||
| b62427c5f4 | |||
| f126e976fd | |||
| 0a2373572f | |||
| 73d2de6dfb | |||
| 49e648689a | |||
| d3cc38aed5 | |||
| a9620246c0 | |||
| 2d649eb0ff | |||
| 66b6579f27 | |||
| 4f9695aabe | |||
| 29ff1bb50a | |||
| fefb665485 | |||
| 52e8fb4a3b | |||
| 8a11c176aa | |||
| 8db9d1a52c | |||
| 10dca5c692 | |||
| 53751d566c | |||
| 12a1e61b68 | |||
| 4f88f26b71 | |||
| 80fcabde7e | |||
| 2e35462300 | |||
| f8f613ec9d | |||
| a1bf38023c | |||
| f032f71136 | |||
| 2e5530cf91 | |||
| c45217e98e | |||
| 62c16bb9d1 | |||
| c012668340 | |||
| 512e016b5e | |||
| 57ffd50558 | |||
| 5245d44a79 | |||
| b39e8325f8 | |||
| b24c9d8336 | |||
| d8b076d105 | |||
| fcc2fced06 | |||
| ffa11fa20a | |||
| e4b61cfe05 | |||
| c59a88bbcb | |||
| a8b1f6f8c2 | |||
| 6e582c4e7c | |||
| f011f5b7f9 | |||
| 1d4a16cd0b | |||
| 2a71734583 | |||
| e9eee6eb3e | |||
| 069ab98da1 | |||
| 90dd26064d | |||
| de9ec716f5 | |||
| 501222a4ee | |||
| 62c595bdf6 | |||
| 06eec88d56 | |||
| 158971d904 | |||
| 6d8d608cc9 | |||
| c9edc407b4 | |||
| c774480a5a | |||
| fa42f9b941 | |||
| 3d3569bbc0 | |||
| a524dec16d | |||
| 4e40aba544 | |||
| 6c164313de | |||
| be98655c75 | |||
| 333212a1de | |||
| a33a4167c1 | |||
| 2cf8bcf746 | |||
| b1457a5feb | |||
| fac9d5fa42 | |||
| 4454eac8af | |||
| fa36c10c01 | |||
| 02cc4a759d | |||
| 04a1f5e014 | |||
| b7ab537b95 | |||
| b0665b2541 | |||
| 77e3fe12c3 | |||
| 6a070e39da | |||
| 3b69286a8e | |||
| 0de2e87221 | |||
| 9daa6aaf8c | |||
| a12ae51f3a | |||
| 17c316c4af | |||
| c438a8f6aa | |||
| 966ad7545c | |||
| 0a17249fb9 | |||
| b19be7dd4b | |||
| 8c29358594 | |||
| d8637923bd | |||
| 9d86e21657 | |||
| ef193b9f03 | |||
| 4828af71b8 | |||
| 3312a47f38 | |||
| c790180e86 | |||
| ef339e128d | |||
| 7c9c7eac10 | |||
| 986830aa47 | |||
| 1e44fec15f | |||
| 39dbf57d7f | |||
| 289c8d5672 | |||
| afa0c6c83f | |||
| 1afa6f7fad | |||
| ee24ceaff1 | |||
| ecdd8ae49c | |||
| e1d070b3af | |||
| 4149c51a7b | |||
| ae5a57277b | |||
| 7b19cd4a21 | |||
| 408250d7ed | |||
| ae91e17ac0 | |||
| b4a9c86c2a | |||
| 1d79e8de1d | |||
| e4ab86c3eb | |||
| d8599a850a | |||
| ae560c2451 | |||
| bcb4d4492d | |||
| 176e1e7cb8 | |||
| f6d4ef4697 | |||
| e9e8ad771e | |||
| 219cbedbcd | |||
| d53f7fc72f | |||
| 3eb2930640 | |||
| 4e4117b5b0 | |||
| 2ba23654ce | |||
| ac11559754 | |||
| fa1552e276 | |||
| 9a4d961dee | |||
| 33c0d5513c | |||
| d679c9f278 | |||
| c26e174885 | |||
| b976600099 | |||
| aac7215be7 | |||
| e116311dc2 | |||
| 4afeded080 | |||
| 83757b5208 | |||
| f335bc23eb | |||
| d793824520 | |||
| 901b61a172 | |||
| 39dd583e77 | |||
| fc3b090da5 | |||
| 16db7af070 | |||
| 0ae4fe0831 | |||
| f01dae5f97 | |||
| 42b4206e66 | |||
| 46572fd992 | |||
| b2540f1d43 | |||
| ad10ffbba3 | |||
| 8a3c7d35ec | |||
| d853b43e17 | |||
| e6187555ce | |||
| bba8fb15e5 | |||
| f70433d901 | |||
| 393f8a6d1b | |||
| e73d3e0823 | |||
| ee4a470fd2 | |||
| 40171720fb | |||
| 09cb5f10aa | |||
| 1228b45045 | |||
| 19afb80597 | |||
| 9495742cb7 | |||
| 01db13ed6c | |||
| 4a1f775c85 | |||
| 495e11c6fe | |||
| 15893c18c9 | |||
| 026e60704b | |||
| 21b155e64b | |||
| 1a1bf8754e | |||
| a51c7c44ec | |||
| 4d1241d712 | |||
| 80747bb441 | |||
| 00f64b1897 | |||
| 01bfe1cc3b | |||
| a0cf6c0f56 | |||
| c72befb5b4 | |||
| 3a52d800e0 | |||
| b6991652ac | |||
| c486d6cf81 | |||
| e7ed3d6ab2 | |||
| 2d30514d64 | |||
| 59b0eeea2b | |||
| 0e77597a70 | |||
| b7de8b40d8 | |||
| 87295252aa | |||
| 7ab96fac8b | |||
| 99194eaf80 | |||
| 50196493e3 | |||
| 75348620d7 | |||
| 4cb045de6c | |||
| ae0cca778e | |||
| 74503f12d6 | |||
| a5f8eb60c6 | |||
| c83ebd73be | |||
| be68353b38 | |||
| 2307035717 | |||
| e708911429 | |||
| 79cfcad026 | |||
| 37afcc76c3 | |||
| f7eb89c113 | |||
| c9c343b840 | |||
| 5c3b1c489f | |||
| dd3a0079db | |||
| bce58c585d | |||
| b720c1411b | |||
| 82c1645d92 | |||
| a8fee20133 | |||
| b0e8a460fd | |||
| b2c38cd06f | |||
| 7ba7b884a4 | |||
| 3daf1a518e | |||
| c6fcb17b4d | |||
| 8b09a8d315 | |||
| 215580215e | |||
| c832c4adeb | |||
| eda3f0f1be | |||
| d0d201bf3a | |||
| c7b04c5bc6 | |||
| c37950a230 | |||
| f88af89562 | |||
| 7da95b80b0 | |||
| 883d68032a | |||
| c74ed18a16 | |||
| e45a9ba5e4 | |||
| de7076e513 | |||
| 6706f361d8 | |||
| 15e6372c30 | |||
| 6dd3593f70 | |||
| 5d376f8783 | |||
| 304d14a6fe | |||
| 4bd6fe8c35 | |||
| 0b0ecd3bc4 | |||
| 58369b8ffa | |||
| cbb4f7421e | |||
| 459605f133 | |||
| 943c0e05b9 | |||
| 9143248e1d | |||
| 4b9d0d2064 | |||
| c8f596ad2a | |||
| a65d00edc8 | |||
| 1e9d02b4b7 | |||
| 135fc2d5d3 | |||
| 189c152745 | |||
| f0f6ec53cb | |||
| a0556ea1f4 | |||
| 81ce36e326 | |||
| 48c31373bf | |||
| d7ce8c667a | |||
| 6290199074 | |||
| 4f3dfb4221 | |||
| a13a51a16f | |||
| 896a0c6de2 | |||
| 9c5dc0dbb5 | |||
| 81003eac63 | |||
| e322e0d078 | |||
| 17e6eef0c5 | |||
| 6114d706ad | |||
| d14fa2ed2b | |||
| 537fc617ff | |||
| 7a6a35568f | |||
| d2c485fdf0 | |||
| 0c49978033 | |||
| 00de4782e7 | |||
| c546bd6b3c | |||
| 258324f092 | |||
| 12a69b7c6c | |||
| b148a09e84 | |||
| adc995dbe7 | |||
| 9cbc703a63 | |||
| 41e6848d75 | |||
| fa21fe8da4 | |||
| ca5b236565 | |||
| 714072aea1 | |||
| 8337fa5f55 | |||
| a9f0696b38 | |||
| 54bff12e1a | |||
| aa17143532 | |||
| 4381e42aaf | |||
| 24ed2cccbe | |||
| a9b0c084f8 | |||
| 16823fa84a | |||
| c30fcd81b2 | |||
| 7f5ae94706 | |||
| 6060ec0f7e | |||
| ef249fee12 | |||
| 71df86c8df | |||
| d61c0ab844 | |||
| b653cc1dab | |||
| 51bce1a2c7 | |||
| e76d881d8a | |||
| 392e211181 | |||
| cebe738beb | |||
| 6e5875a7b7 | |||
| b2044ac7bd | |||
| 8a467c2d65 | |||
| db8cb56984 | |||
| d1d152ef5a | |||
| 82950b0ec0 | |||
| 3110b96305 | |||
| 7e44226a6d | |||
| 3f76cb58fe | |||
| 3cf8f4c89b | |||
| 90ae7c469a | |||
| ae967c5ddb | |||
| e801ba08ad | |||
| e6f5b2493b | |||
| 5f677f5076 | |||
| 2de32dc944 | |||
| ab534e203a | |||
| eaf41805d7 | |||
| 631ebb9346 | |||
| 7e53743b07 | |||
| 70e1aca6c7 | |||
| d0d323954b | |||
| d1c24bc486 | |||
| f998d32b0a | |||
| 3226b22dfb | |||
| 9a51b2876f | |||
| ab9fbc9032 | |||
| d2f24723fc | |||
| 5e710a792f | |||
| 3c8bdfbdac | |||
| 204369e0fe | |||
| 1e2ea79a6a | |||
| 22f1a32e1b | |||
| c24c0817ae | |||
| e53325cdc5 | |||
| 6687f4af98 | |||
| 575a317eed | |||
| bdb3ab1a50 | |||
| f486a659d0 | |||
| 58b4204aab | |||
| c249585bc2 | |||
| 9c114628c7 | |||
| b35d513e44 | |||
| a904c35b7f | |||
| 2e595b5e86 | |||
| 46fa3e57cd | |||
| 79b6d6917d | |||
| 9f24d22a2c | |||
| 06f1d3f6a4 | |||
| e29613a37e | |||
| 6e6675d0a7 | |||
| 32d4c0268f | |||
| 2ab26153fd | |||
| a29bbc9a42 | |||
| e7e0e308c7 | |||
| 1ade8f8a38 | |||
| 791e8e787c | |||
| a2c43a7f1e | |||
| 12202c2021 | |||
| 4127075b13 | |||
| d9fa685ae6 | |||
| 2f4daea253 | |||
| bac72b8eb3 | |||
| 5af9c3ee27 | |||
| 1a76913f3f | |||
| 8b403a4208 | |||
| 0bab704163 | |||
| d550dcbf48 | |||
| 7fdbfe6dfb | |||
| 4e6d2088e1 | |||
| 67b180bf7c | |||
| 7853899486 | |||
| 9a04724095 | |||
| 831995e8e4 | |||
| c8cdb3e772 | |||
| e5b3af3239 | |||
| 0740f2d094 | |||
| 25a97e34c7 | |||
| 1ee1cf9ea9 | |||
| e4a1138d8d | |||
| 41bb05413c | |||
| c746bd94b2 | |||
| acea3a3063 | |||
| b269728ecd | |||
| e7a861d8a1 | |||
| 1a5a76c0f1 | |||
| 838d7277c3 | |||
| 1672705464 | |||
| 9ef4484fb3 | |||
| 645668e1f9 | |||
| fb29cea8d2 | |||
| 1ecdff855e | |||
| 7c6e079f56 | |||
| 41f8067727 | |||
| f733b0750a | |||
| 83d31b7047 | |||
| 966e0886e1 | |||
| a67d353bcb | |||
| ac2f246988 | |||
| e0ce571000 | |||
| 1bcfd9bbb4 | |||
| c561c47eae | |||
| c3338d3e05 | |||
| ba9ae07455 | |||
| c64ff527dd | |||
| f27e34128e | |||
| c8db992b17 | |||
| d76f897840 | |||
| 5e0cc3699f | |||
| 895b7afa25 | |||
| a088189ed1 | |||
| 406cfad31a | |||
| 6c9c0e1839 | |||
| eb02a8b6f7 | |||
| 73f379832c | |||
| 4233c19dc9 | |||
| 403765a2b5 | |||
| d30d7328cf | |||
| 376fa29f7e | |||
| 16d72ebf6f | |||
| 52ad696178 | |||
| 2b3aa9c3ee | |||
| 6fe85fac13 | |||
| 9964e1797a | |||
| e4f554bcd4 | |||
| c25b49c179 | |||
| a573551110 | |||
| 881e2bfc4a | |||
| 474c2a1f7d | |||
| c4de085e11 | |||
| 0676411dd5 | |||
| f05d367a5d | |||
| edb5f30d6c | |||
| 7abe9b7dc6 | |||
| caf68d438f | |||
| fa60f884c1 | |||
| c77968940e | |||
| cfb9b53b54 | |||
| caac9c20e6 | |||
| 8bf7603f66 | |||
| 8c662a5152 | |||
| da92874a40 | |||
| 5113bf4d3f | |||
| 1bdec0c9ae | |||
| 57dbcf3624 | |||
| 501a68267e | |||
| a2a57f758c | |||
| 157dfc75ed | |||
| f5ce898bd2 | |||
| c6a0c542aa | |||
| b0e11095f4 | |||
| 79acdb4b7b | |||
| 19db9a4eac | |||
| 1e0b342bbc | |||
| 23d5a41d56 | |||
| b7a30cc73a | |||
| 93beb86f91 | |||
| 0577f6ce1d | |||
| 76dd2e4c54 | |||
| 156de6112b | |||
| eb0f04310e | |||
| 27d2792a9c | |||
| ec387637ed | |||
| 64e6724664 | |||
| f319a9b5d1 | |||
| e2b35e765c | |||
| 8bfce061e6 | |||
| 64a32fd214 | |||
| 2ee88a2742 | |||
| aa21088e99 | |||
| 06dc869b84 | |||
| df73ee1fdf | |||
| cf78687315 | |||
| 66e6fa84c8 | |||
| dcd5d2692f | |||
| 3c4333021f | |||
| 56a9fc2349 | |||
| 24144f01d4 | |||
| 6f0467918b | |||
| 53ee4cdc1b | |||
| b1a3796eca | |||
| 89318407ea | |||
| 6dcb97bcac | |||
| 1869fa8dc5 | |||
| 4b4b74d07c | |||
| ff9e35eb52 | |||
| 19bca9ec73 | |||
| 6facfd3d3c | |||
| b2f0bd6698 | |||
| e7085ab4ff | |||
| 44e07ddc50 | |||
| a4ff4f7b2a | |||
| 754e3d526b | |||
| 478f52c94b | |||
| 757893c757 | |||
| a1dc13ceb4 | |||
| 157235433e | |||
| 57831646d9 | |||
| 095190d757 | |||
| 27f58051ad | |||
| a9cdea7318 | |||
| 7dbf880228 | |||
| aacdbf0742 | |||
| c17ffc6aff | |||
| 1ea9ee3069 | |||
| 558a1788dc | |||
| e2036ab3dc | |||
| ddbf8b0896 | |||
| 3f97ec45f8 | |||
| 7ceb25ea71 |
@@ -40,8 +40,5 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# prettier
|
|
||||||
.prettierrc
|
|
||||||
|
|
||||||
# idea
|
# idea
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
+157
-69
@@ -1,76 +1,164 @@
|
|||||||
stages: [notify]
|
stages:
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
# --- Notify when MR is opened/updated ---
|
.build_template: &build_template
|
||||||
notify_discord_mr:
|
stage: build
|
||||||
stage: notify
|
image: node:20-alpine
|
||||||
image: alpine:3.20
|
cache:
|
||||||
rules:
|
key: npm-cache
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
paths:
|
||||||
|
- node_modules/
|
||||||
variables:
|
variables:
|
||||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
NPM_CONFIG_PRODUCTION: 'false'
|
||||||
before_script:
|
NODE_ENV: ''
|
||||||
- apk add --no-cache curl jq
|
script:
|
||||||
script: |
|
- echo "Installing dependencies..."
|
||||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
- npm ci --no-audit --no-fund
|
||||||
|
- echo "Build env used:"
|
||||||
|
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
||||||
|
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
||||||
|
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
||||||
|
- echo "Building Next.js static export..."
|
||||||
|
- npx next build
|
||||||
|
- |
|
||||||
|
mkdir -p out
|
||||||
|
cat <<EOF > out/build-info.json
|
||||||
|
{
|
||||||
|
"commit": "$CI_COMMIT_SHORT_SHA",
|
||||||
|
"pipeline": "$CI_PIPELINE_ID",
|
||||||
|
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||||
|
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
||||||
|
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
||||||
|
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
artifacts:
|
||||||
|
name: 'out-$CI_COMMIT_SHORT_SHA'
|
||||||
|
paths:
|
||||||
|
- out/
|
||||||
|
expire_in: 1 week
|
||||||
|
|
||||||
jq -n \
|
.deploy_template: &deploy_template
|
||||||
--arg repo "$CI_PROJECT_PATH" \
|
stage: deploy
|
||||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
image:
|
||||||
--arg url "$MR_URL" \
|
name: amazon/aws-cli:latest
|
||||||
--arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
|
entrypoint: ['/bin/sh', '-c']
|
||||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
script:
|
||||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
- set -e
|
||||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
- aws --version
|
||||||
'{
|
- echo "Cleaning up newline characters in AWS credentials..."
|
||||||
username: "CI Bot - FE",
|
- export AWS_ACCESS_KEY_ID=$(echo $AWS_ACCESS_KEY_ID | tr -d '\r\n')
|
||||||
embeds: [{
|
- export AWS_SECRET_ACCESS_KEY=$(echo $AWS_SECRET_ACCESS_KEY | tr -d '\r\n')
|
||||||
title: "📣 [LTI WEB CLIENT] Merge Request Opened/Updated",
|
- echo "Deploying to s3://$S3_BUCKET in region $AWS_REGION"
|
||||||
description: ($mr + " in " + $repo),
|
- aws s3api head-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" || aws s3api create-bucket --bucket "$S3_BUCKET" --region "$AWS_REGION" --create-bucket-configuration LocationConstraint="$AWS_REGION"
|
||||||
url: $url,
|
- aws s3 sync ./out "s3://$S3_BUCKET" --delete --region "$AWS_REGION" --endpoint-url "https://s3.ap-southeast-3.amazonaws.com"
|
||||||
color: 3447003,
|
|
||||||
fields: [
|
|
||||||
{name: "Author", value: $requestor, inline: true},
|
|
||||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
|
||||||
{name: "Title", value: $title}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}' \
|
|
||||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
|
||||||
|
|
||||||
# --- Notify when MR is merged ---
|
# CloudFront invalidation
|
||||||
notify_discord_merge:
|
- |
|
||||||
stage: notify
|
STATUS="success"
|
||||||
image: alpine:3.20
|
if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then
|
||||||
|
echo "Invalidating CloudFront cache..."
|
||||||
|
if ! aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" --paths "/*"; then
|
||||||
|
echo "CloudFront invalidation failed."
|
||||||
|
STATUS="failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No CloudFront distribution specified — skipping invalidation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Notifikasi Discord
|
||||||
|
- |
|
||||||
|
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}"
|
||||||
|
|
||||||
|
if [ "$CI_COMMIT_BRANCH" = "development" ]; then
|
||||||
|
ENVIRONMENT_NAME="WEB-LTI-DEV"
|
||||||
|
elif [ "$CI_COMMIT_BRANCH" = "master" ]; then
|
||||||
|
ENVIRONMENT_NAME="WEB-LTI-PROD"
|
||||||
|
else
|
||||||
|
ENVIRONMENT_NAME="UNKNOWN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$STATUS" = "success" ]; then
|
||||||
|
COLOR=3066993
|
||||||
|
TITLE="✅ Deployment ${ENVIRONMENT_NAME} Succeeded"
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully."
|
||||||
|
else
|
||||||
|
COLOR=15158332
|
||||||
|
TITLE="❌ Deployment ${ENVIRONMENT_NAME} Failed"
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` encountered issues."
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg title "$TITLE" \
|
||||||
|
--arg desc "$DESC" \
|
||||||
|
--arg color "$COLOR" \
|
||||||
|
--arg repo "$CI_PROJECT_PATH" \
|
||||||
|
--arg actor "$GITLAB_USER_LOGIN" \
|
||||||
|
--arg commit "$CI_COMMIT_SHA" \
|
||||||
|
--arg run_url "$RUN_URL" \
|
||||||
|
'{
|
||||||
|
username: "CI Bot - LTI WEB",
|
||||||
|
embeds: [{
|
||||||
|
title: $title,
|
||||||
|
description: $desc,
|
||||||
|
color: ($color|tonumber),
|
||||||
|
fields: [
|
||||||
|
{name: "Repository", value: $repo, inline: true},
|
||||||
|
{name: "Actor", value: $actor, inline: true},
|
||||||
|
{name: "Commit", value: $commit, inline: false},
|
||||||
|
{name: "Pipeline", value: ("[Open run](" + $run_url + ")"), inline: false}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}' > payload.json
|
||||||
|
|
||||||
|
curl -sS -H "Content-Type: application/json" -d @payload.json "$DISCORD_WEBHOOK_URL"
|
||||||
|
|
||||||
|
# ====== DEVELOPMENT (Branch development) ======
|
||||||
|
build:dev:
|
||||||
|
<<: *build_template
|
||||||
rules:
|
rules:
|
||||||
# Only run for merge request pipelines that are in merged state
|
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
|
environment:
|
||||||
|
name: development
|
||||||
variables:
|
variables:
|
||||||
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
|
# NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id'
|
||||||
before_script:
|
# NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id'
|
||||||
- apk add --no-cache curl jq
|
NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id'
|
||||||
script: |
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
||||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
||||||
|
|
||||||
|
deploy:dev:
|
||||||
|
<<: *deploy_template
|
||||||
|
needs: ['build:dev']
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||||
|
variables:
|
||||||
|
S3_BUCKET: 'dev-lti-erp.mbugroup.id'
|
||||||
|
AWS_REGION: 'ap-southeast-3'
|
||||||
|
CLOUDFRONT_DISTRIBUTION_ID: 'E1Z8XTA8XF1GIV'
|
||||||
|
environment:
|
||||||
|
name: development
|
||||||
|
url: https://dev-lti-erp.mbugroup.id
|
||||||
|
# ====== PRODUCTION ======
|
||||||
|
# build:production:
|
||||||
|
# <<: *build_template
|
||||||
|
# rules:
|
||||||
|
# # pilih salah satu: pakai branch master ATAU pakai tags rilis
|
||||||
|
# - if: '$CI_COMMIT_BRANCH == "master"'
|
||||||
|
# # - if: '$CI_COMMIT_TAG' # kalau mau rilis via tag, uncomment ini dan hapus baris di atas
|
||||||
|
# environment:
|
||||||
|
# name: production
|
||||||
|
|
||||||
|
# deploy:production:
|
||||||
|
# <<: *deploy_template
|
||||||
|
# needs: ["build:production"]
|
||||||
|
# rules:
|
||||||
|
# - 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
|
||||||
|
|
||||||
jq -n \
|
|
||||||
--arg repo "$CI_PROJECT_PATH" \
|
|
||||||
--arg mr "#${CI_MERGE_REQUEST_IID}" \
|
|
||||||
--arg url "$MR_URL" \
|
|
||||||
--arg requestor "${CI_MERGE_REQUEST_AUTHOR}" \
|
|
||||||
--arg source "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" \
|
|
||||||
--arg target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \
|
|
||||||
--arg title "$CI_MERGE_REQUEST_TITLE" \
|
|
||||||
'{
|
|
||||||
username: "CI Bot - FE",
|
|
||||||
embeds: [{
|
|
||||||
title: "✅ [LTI WEB CLIENT] Merge Request Merged",
|
|
||||||
description: ($mr + " has been merged into " + $repo),
|
|
||||||
url: $url,
|
|
||||||
color: 3066993,
|
|
||||||
fields: [
|
|
||||||
{name: "Author", value: $requestor, inline: true},
|
|
||||||
{name: "Source → Target", value: ($source + " → " + $target), inline: true},
|
|
||||||
{name: "Title", value: $title}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}' \
|
|
||||||
| curl -sS -H "Content-Type: application/json" -d @- "$WEBHOOK_URL"
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
npm run format
|
||||||
|
npm run lint
|
||||||
|
npm run build
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache git bash build-base curl
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Buat config agar Next tahu output: export
|
||||||
|
RUN echo "const config = { output: 'export', images: { unoptimized: true } }; export default config;" > next.config.mjs
|
||||||
|
|
||||||
|
# Build project (Next.js 15 otomatis static export)
|
||||||
|
RUN NEXT_DISABLE_TURBOPACK=1 npx next build
|
||||||
|
|
||||||
|
# Copy static assets dan hasil build agar bisa diakses
|
||||||
|
RUN mkdir -p .next/server/app/_next && \
|
||||||
|
cp -r .next/static .next/server/app/_next/static && \
|
||||||
|
cp -r public/* .next/server/app/
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "VERCEL_GIT_COMMIT_REF: $VERCEL_GIT_COMMIT_REF"
|
||||||
|
|
||||||
|
if [[ "$VERCEL_GIT_COMMIT_REF" == "master" || "$VERCEL_GIT_COMMIT_REF" == "development" ]]; then
|
||||||
|
echo "✅ - Build can proceed"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "🛑 - Build cancelled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
dev-web-lti:
|
||||||
|
container_name: dev-web-lti
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- '3002:3000'
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
APP_ENV: production
|
||||||
|
networks:
|
||||||
|
- dev-lti-network
|
||||||
|
restart: always
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '3.0'
|
||||||
|
memory: 3G
|
||||||
|
reservations:
|
||||||
|
cpus: '1.0'
|
||||||
|
memory: 512M
|
||||||
|
extra_hosts:
|
||||||
|
- 'host.docker.internal:host-gateway'
|
||||||
|
# Optional: aktifkan healthcheck jika punya endpoint
|
||||||
|
# healthcheck:
|
||||||
|
# test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/healthz || exit 1"]
|
||||||
|
# interval: 10s
|
||||||
|
# timeout: 3s
|
||||||
|
# retries: 10
|
||||||
|
# start_period: 15s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dev-lti-network:
|
||||||
|
external: true
|
||||||
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
import { dirname } from "path";
|
import { dirname } from 'path';
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from 'url';
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -10,14 +10,14 @@ const compat = new FlatCompat({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
"node_modules/**",
|
'node_modules/**',
|
||||||
".next/**",
|
'.next/**',
|
||||||
"out/**",
|
'out/**',
|
||||||
"build/**",
|
'build/**',
|
||||||
"next-env.d.ts",
|
'next-env.d.ts',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Generated
+1122
-571
File diff suppressed because it is too large
Load Diff
+12
-4
@@ -6,19 +6,25 @@
|
|||||||
"dev": "eslint && next dev --turbopack",
|
"dev": "eslint && next dev --turbopack",
|
||||||
"build": "next build --turbopack",
|
"build": "next build --turbopack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"prepare": "husky",
|
||||||
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
"@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",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"next": "15.5.3",
|
"next": "^15.5.7",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"react-number-format": "^5.4.4",
|
||||||
"react-select": "^5.10.2",
|
"react-select": "^5.10.2",
|
||||||
"swr": "^2.3.6",
|
"swr": "^2.3.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@@ -33,9 +39,11 @@
|
|||||||
"@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.5",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.3",
|
"eslint-config-next": "^15.5.7",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
const config = {
|
const config = {
|
||||||
plugins: ["@tailwindcss/postcss"],
|
plugins: ['@tailwindcss/postcss'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
import ExpenseRequestForm from '@/components/pages/expense/form/ExpenseRequestForm';
|
||||||
|
|
||||||
|
const AddExpense = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<ExpenseRequestForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddExpense;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import ExpenseRequestForm from '@/components/pages/expense/form/ExpenseRequestForm';
|
||||||
|
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const ExpenseEditPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
|
expenseId,
|
||||||
|
(id: number) => ExpenseApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!expenseId) {
|
||||||
|
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 (!isLoadingExpense && (!expense || isResponseError(expense))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpenseCanBeEdited =
|
||||||
|
!isLoadingExpense &&
|
||||||
|
isResponseSuccess(expense) &&
|
||||||
|
expense.data.latest_approval.step_number !== 5 &&
|
||||||
|
(expense.data.latest_approval.step_number === 1 ||
|
||||||
|
expense.data.latest_approval.step_number === 2 ||
|
||||||
|
expense.data.latest_approval.step_number === 3);
|
||||||
|
|
||||||
|
if (!isLoadingExpense && !isExpenseCanBeEdited) {
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingExpense && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingExpense && isResponseSuccess(expense) && (
|
||||||
|
<ExpenseRequestForm type='edit' initialValues={expense.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseEditPage;
|
||||||
@@ -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 { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import ExpenseDetail from '@/components/pages/expense/ExpenseDetail';
|
||||||
|
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const ExpenseDetailPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
|
expenseId,
|
||||||
|
(id: number) => ExpenseApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!expenseId) {
|
||||||
|
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 (!isLoadingExpense && (!expense || isResponseError(expense))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingExpense && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingExpense && isResponseSuccess(expense) && (
|
||||||
|
<ExpenseDetail initialValues={expense.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseDetailPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ExpensesTable from '@/components/pages/expense/ExpensesTable';
|
||||||
|
|
||||||
|
const Expense = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<ExpensesTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Expense;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import ExpenseRealizationForm from '@/components/pages/expense/form/ExpenseRealizationForm';
|
||||||
|
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const ExpenseRealizationEditPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
|
expenseId,
|
||||||
|
(id: number) => ExpenseApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!expenseId) {
|
||||||
|
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 (!isLoadingExpense && (!expense || isResponseError(expense))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpenseRealizationCanBeEdited =
|
||||||
|
!isLoadingExpense &&
|
||||||
|
isResponseSuccess(expense) &&
|
||||||
|
expense.data.latest_approval.action !== 'REJECTED' &&
|
||||||
|
(expense.data.latest_approval.step_number === 4 ||
|
||||||
|
expense.data.latest_approval.step_number === 5);
|
||||||
|
|
||||||
|
if (!isLoadingExpense && !isExpenseRealizationCanBeEdited) {
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingExpense && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingExpense && isResponseSuccess(expense) && (
|
||||||
|
<ExpenseRealizationForm type='edit' initialValues={expense.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseRealizationEditPage;
|
||||||
@@ -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,67 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import ExpenseRealizationForm from '@/components/pages/expense/form/ExpenseRealizationForm';
|
||||||
|
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const ExpenseRealization = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
|
expenseId,
|
||||||
|
(id: number) => ExpenseApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!expenseId) {
|
||||||
|
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 (!isLoadingExpense && (!expense || isResponseError(expense))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpenseCanBeRealized =
|
||||||
|
isResponseSuccess(expense) &&
|
||||||
|
expense.data.latest_approval.action !== 'REJECTED' &&
|
||||||
|
expense.data.latest_approval.step_number === 3;
|
||||||
|
|
||||||
|
if (isResponseSuccess(expense) && !isExpenseCanBeRealized) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingExpense && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingExpense && isResponseSuccess(expense) && (
|
||||||
|
<ExpenseRealizationForm initialValues={expense.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseRealization;
|
||||||
@@ -1,5 +1,41 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@plugin "daisyui";
|
@plugin "daisyui";
|
||||||
|
@import '../styles/daisyui.css';
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: 'lti';
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: 'light';
|
||||||
|
--color-base-100: oklch(98% 0.001 106.423);
|
||||||
|
--color-base-200: oklch(97% 0.001 106.424);
|
||||||
|
--color-base-300: oklch(92% 0.003 48.717);
|
||||||
|
--color-base-content: oklch(22.389% 0.031 278.072);
|
||||||
|
--color-primary: oklch(60% 0.126 221.723);
|
||||||
|
--color-primary-content: oklch(100% 0 0);
|
||||||
|
--color-secondary: oklch(52% 0.105 223.128);
|
||||||
|
--color-secondary-content: oklch(100% 0 0);
|
||||||
|
--color-accent: oklch(45% 0.085 224.283);
|
||||||
|
--color-accent-content: oklch(100% 0 0);
|
||||||
|
--color-neutral: oklch(39% 0.07 227.392);
|
||||||
|
--color-neutral-content: oklch(100% 0 0);
|
||||||
|
--color-info: oklch(58% 0.158 241.966);
|
||||||
|
--color-info-content: oklch(100% 0 0);
|
||||||
|
--color-success: oklch(62% 0.194 149.214);
|
||||||
|
--color-success-content: oklch(100% 0 0);
|
||||||
|
--color-warning: oklch(85% 0.199 91.936);
|
||||||
|
--color-warning-content: oklch(0% 0 0);
|
||||||
|
--color-error: oklch(57% 0.245 27.325);
|
||||||
|
--color-error-content: oklch(100% 0 0);
|
||||||
|
--radius-selector: 0rem;
|
||||||
|
--radius-field: 0.25rem;
|
||||||
|
--radius-box: 0.25rem;
|
||||||
|
--size-selector: 0.21875rem;
|
||||||
|
--size-field: 0.1875rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-primary: #1f74bf;
|
--color-primary: #1f74bf;
|
||||||
@@ -12,3 +48,8 @@
|
|||||||
html {
|
html {
|
||||||
scrollbar-gutter: initial;
|
scrollbar-gutter: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-select__menu-portal {
|
||||||
|
position: relative;
|
||||||
|
z-index: 99999 !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import InventoryAdjustmentForm from "@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm";
|
import InventoryAdjustmentForm from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm';
|
||||||
|
|
||||||
const CreateInventoryAdjustment = () => {
|
const CreateInventoryAdjustment = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4 flex flex-row justify-center">
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
<InventoryAdjustmentForm/>
|
<InventoryAdjustmentForm />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CreateInventoryAdjustment;
|
export default CreateInventoryAdjustment;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -7,11 +7,12 @@ import type { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
|||||||
|
|
||||||
const DetailInventoryAdjustment = () => {
|
const DetailInventoryAdjustment = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [inventoryAdjustment, setInventoryAdjustment] = useState<InventoryAdjustment | null>(null);
|
const [inventoryAdjustment, setInventoryAdjustment] =
|
||||||
|
useState<InventoryAdjustment | null>(null);
|
||||||
|
|
||||||
// Ambil data dari router state
|
// Ambil data dari router state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Router State");
|
console.log('Router State');
|
||||||
console.log(window.history.state);
|
console.log(window.history.state);
|
||||||
const state = window.history.state?.usr as
|
const state = window.history.state?.usr as
|
||||||
| { inventoryAdjustment?: InventoryAdjustment }
|
| { inventoryAdjustment?: InventoryAdjustment }
|
||||||
@@ -25,19 +26,19 @@ const DetailInventoryAdjustment = () => {
|
|||||||
|
|
||||||
const finalData = inventoryAdjustment;
|
const finalData = inventoryAdjustment;
|
||||||
|
|
||||||
console.log("Final Data");
|
console.log('Final Data');
|
||||||
console.log(finalData);
|
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'>
|
||||||
<span className="loading loading-spinner loading-xl" />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4 flex flex-row justify-center">
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
<InventoryAdjustmentForm initialValues={finalData} />
|
<InventoryAdjustmentForm initialValues={finalData} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import MovementForm from '@/components/pages/inventory/movement/form/MovementForm';
|
||||||
|
|
||||||
|
const AddMovement = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<MovementForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddMovement;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import MovementForm from '@/components/pages/inventory/movement/form/MovementForm';
|
||||||
|
import { MovementApi } from '@/services/api/inventory';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const MovementEdit = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const movementId = searchParams.get('movementId');
|
||||||
|
|
||||||
|
const { data: movement, isLoading: isLoadingMovement } = useSWR(
|
||||||
|
movementId,
|
||||||
|
(id: number) => MovementApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!movementId) {
|
||||||
|
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 (!isLoadingMovement && (!movement || isResponseError(movement))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingMovement && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingMovement && isResponseSuccess(movement) && (
|
||||||
|
<MovementForm type='edit' initialValues={movement.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovementEdit;
|
||||||
@@ -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,48 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import MovementForm from '@/components/pages/inventory/movement/form/MovementForm';
|
||||||
|
import { MovementApi } from '@/services/api/inventory';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const MovementDetail = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const movementId = searchParams.get('movementId');
|
||||||
|
|
||||||
|
const { data: movement, isLoading: isLoadingMovement } = useSWR(
|
||||||
|
movementId,
|
||||||
|
(id: number) => MovementApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!movementId) {
|
||||||
|
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 (!isLoadingMovement && (!movement || isResponseError(movement))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingMovement && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingMovement && isResponseSuccess(movement) && (
|
||||||
|
<MovementForm type='detail' initialValues={movement.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovementDetail;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import MovementTable from '@/components/pages/inventory/movement/MovementTable';
|
||||||
|
|
||||||
|
const Movement = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<MovementTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Movement;
|
||||||
+1
-1
@@ -28,7 +28,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang='en'>
|
<html lang='en' data-theme='lti'>
|
||||||
<body className={`${inter.variable} antialiased font-inter`}>
|
<body className={`${inter.variable} antialiased font-inter`}>
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<MainDrawer>{children}</MainDrawer>
|
<MainDrawer>{children}</MainDrawer>
|
||||||
|
|||||||
@@ -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,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditMarketingDelivery = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(`get-so-${soId}`, () =>
|
||||||
|
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingForm
|
||||||
|
formType='add_deliver'
|
||||||
|
initialValues={marketing.data}
|
||||||
|
afterSubmit={() => {
|
||||||
|
refreshMarketing();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditMarketingDelivery;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
|
||||||
|
const AddSalesOrder = () => {
|
||||||
|
return (
|
||||||
|
<div className='size-full p-4'>
|
||||||
|
<MarketingForm formType='add' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddSalesOrder;
|
||||||
@@ -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,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditMarketingDelivery = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(`get-so-${soId}`, () =>
|
||||||
|
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isResponseSuccess(marketing) &&
|
||||||
|
marketing.data.latest_approval.step_number != 3
|
||||||
|
) {
|
||||||
|
toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!');
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingForm
|
||||||
|
formType='edit_deliver'
|
||||||
|
initialValues={marketing.data}
|
||||||
|
afterSubmit={() => {
|
||||||
|
refreshMarketing();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditMarketingDelivery;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const DetailMarketing = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingDetail
|
||||||
|
initialValues={marketing.data}
|
||||||
|
refresh={refreshMarketing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailMarketing;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditSalesOrder = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(`get-so-${soId}`, () =>
|
||||||
|
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingForm
|
||||||
|
formType='edit'
|
||||||
|
initialValues={marketing.data}
|
||||||
|
afterSubmit={() => {
|
||||||
|
refreshMarketing();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditSalesOrder;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import MarketingTable from '@/components/pages/marketing/MarketingTable';
|
||||||
|
|
||||||
|
const Marketing = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
<MarketingTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Marketing;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm";
|
import CustomerForm from '@/components/pages/master-data/customer/form/CustomerForm';
|
||||||
|
|
||||||
const AddCustomer = () => {
|
const AddCustomer = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4 flex flex-row justify-center">
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
<CustomerForm/>
|
<CustomerForm />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AddCustomer;
|
export default AddCustomer;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -1,45 +1,47 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr';
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm";
|
import CustomerForm from '@/components/pages/master-data/customer/form/CustomerForm';
|
||||||
|
|
||||||
const CustomerDetail = () => {
|
const CustomerDetail = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const costumerId = searchParams.get("customerId");
|
const costumerId = searchParams.get('customerId');
|
||||||
|
|
||||||
const { data: costumer, isLoading: isLoadingCostumer } = useSWR(
|
const { data: costumer, isLoading: isLoadingCostumer } = useSWR(
|
||||||
costumerId,
|
costumerId,
|
||||||
(id: number) => CustomerApi.getSingle(id)
|
(id: number) => CustomerApi.getSingle(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if(!costumerId){
|
if (!costumerId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
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' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isLoadingCostumer && (!costumer || isResponseError(costumer))){
|
if (!isLoadingCostumer && (!costumer || isResponseError(costumer))) {
|
||||||
router.replace("/404");
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 flex flex-row justify-center">
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
{isLoadingCostumer && <span className="loading loading-spinner loading-xl" />}
|
{isLoadingCostumer && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
{!isLoadingCostumer && isResponseSuccess(costumer) && (
|
{!isLoadingCostumer && isResponseSuccess(costumer) && (
|
||||||
<CustomerForm formType="detail" initialValues={costumer.data} />
|
<CustomerForm formType='detail' initialValues={costumer.data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomerDetail;
|
export default CustomerDetail;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import CustomersTable from "@/components/pages/master-data/customer/CustomersTable";
|
import CustomersTable from '@/components/pages/master-data/customer/CustomersTable';
|
||||||
|
|
||||||
const Customer = () => {
|
const Customer = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4">
|
<section className='w-full p-4'>
|
||||||
<CustomersTable />
|
<CustomersTable />
|
||||||
</section>
|
</section>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Customer;
|
export default Customer;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import FlockForm from '@/components/pages/master-data/flock/form/FlockForm';
|
||||||
|
|
||||||
|
const AddFlock = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<FlockForm />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddFlock;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import FlockForm from '@/components/pages/master-data/flock/form/FlockForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const FlockEdit = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// Get Query Params
|
||||||
|
const flockId = searchParams.get('flockId');
|
||||||
|
|
||||||
|
// Fetch Data
|
||||||
|
const { data: flock, isLoading: isLoadingFlock } = useSWR(
|
||||||
|
flockId,
|
||||||
|
(id: number) => FlockApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!flockId) {
|
||||||
|
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 (!isLoadingFlock && (!flock || isResponseError(flock))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFlock && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingFlock && isResponseSuccess(flock) && (
|
||||||
|
<FlockForm formType='edit' initialValues={flock.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlockEdit;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import FlockForm from '@/components/pages/master-data/flock/form/FlockForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const FlockDetail = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// Get Query Params
|
||||||
|
const flockId = searchParams.get('flockId');
|
||||||
|
|
||||||
|
// Fetch Data
|
||||||
|
const { data: flock, isLoading: isLoadingFlock } = useSWR(
|
||||||
|
flockId,
|
||||||
|
(id: number) => FlockApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!flockId) {
|
||||||
|
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 (!isLoadingFlock && (!flock || isResponseError(flock))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFlock && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingFlock && isResponseSuccess(flock) && (
|
||||||
|
<FlockForm formType='detail' initialValues={flock.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlockDetail;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import FlockTable from '@/components/pages/master-data/flock/FlocksTable';
|
||||||
|
|
||||||
|
const Flock = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<FlockTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Flock;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import ProductCategoryForm from "@/components/pages/master-data/product-category/form/ProductCategoryForm";
|
import ProductCategoryForm from '@/components/pages/master-data/product-category/form/ProductCategoryForm';
|
||||||
|
|
||||||
const AddProductCategory = () => {
|
const AddProductCategory = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 flex flex-row justify-center">
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
<ProductCategoryForm />
|
<ProductCategoryForm />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,39 +9,44 @@ import { ProductCategoryApi } from '@/services/api/master-data';
|
|||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
const ProductCategoryEdit = () => {
|
const ProductCategoryEdit = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const productCategoryId = searchParams.get('productCategoryId');
|
const productCategoryId = searchParams.get('productCategoryId');
|
||||||
|
|
||||||
const { data: productCategory, isLoading: isLoadingProductCategory } = useSWR(
|
const { data: productCategory, isLoading: isLoadingProductCategory } = useSWR(
|
||||||
productCategoryId,
|
productCategoryId,
|
||||||
(id: number) => ProductCategoryApi.getSingle(id)
|
(id: number) => ProductCategoryApi.getSingle(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!productCategoryId) {
|
if (!productCategoryId) {
|
||||||
router.back();
|
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 (!isLoadingProductCategory && (!productCategory || isResponseError(productCategory))) {
|
|
||||||
router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
{isLoadingProductCategory && <span className='loading loading-spinner loading-xl' />}
|
<span className='loading loading-spinner loading-xl' />
|
||||||
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
|
</div>
|
||||||
<ProductCategoryForm type='edit' initialValues={productCategory.data} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isLoadingProductCategory &&
|
||||||
|
(!productCategory || isResponseError(productCategory))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingProductCategory && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
|
||||||
|
<ProductCategoryForm type='edit' initialValues={productCategory.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ProductCategoryEdit;
|
export default ProductCategoryEdit;
|
||||||
@@ -29,16 +29,24 @@ const ProductCategoryDetail = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoadingProductCategory && (!productCategory || isResponseError(productCategory))) {
|
if (
|
||||||
|
!isLoadingProductCategory &&
|
||||||
|
(!productCategory || isResponseError(productCategory))
|
||||||
|
) {
|
||||||
router.replace('/404');
|
router.replace('/404');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
{isLoadingProductCategory && <span className='loading loading-spinner loading-xl' />}
|
{isLoadingProductCategory && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
|
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
|
||||||
<ProductCategoryForm type='detail' initialValues={productCategory.data} />
|
<ProductCategoryForm
|
||||||
|
type='detail'
|
||||||
|
initialValues={productCategory.data}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import ProductCategoryTable from "@/components/pages/master-data/product-category/ProductCategoryTable";
|
import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable';
|
||||||
|
|
||||||
const ProductCategory = () => {
|
const ProductCategory = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4">
|
<section className='w-full p-4'>
|
||||||
<ProductCategoryTable />
|
<ProductCategoryTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ProductForm from '@/components/pages/master-data/product/form/ProductForm
|
|||||||
|
|
||||||
const AddProduct = () => {
|
const AddProduct = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4 flex flex-row justify-center">
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
<ProductForm />
|
<ProductForm />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ const ProductEdit = () => {
|
|||||||
|
|
||||||
const productId = searchParams.get('productId');
|
const productId = searchParams.get('productId');
|
||||||
|
|
||||||
const { data: product, isLoading } = useSWR(
|
const { data: product, isLoading } = useSWR(productId, (id: number) =>
|
||||||
productId,
|
ProductApi.getSingle(id)
|
||||||
(id: number) => ProductApi.getSingle(id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!productId) {
|
if (!productId) {
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ const ProductDetail = () => {
|
|||||||
|
|
||||||
const productId = searchParams.get('productId');
|
const productId = searchParams.get('productId');
|
||||||
|
|
||||||
const { data: product, isLoading } = useSWR(
|
const { data: product, isLoading } = useSWR(productId, (id: number) =>
|
||||||
productId,
|
ProductApi.getSingle(id)
|
||||||
(id: number) => ProductApi.getSingle(id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!productId) {
|
if (!productId) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import ProductsTable from "@/components/pages/master-data/product/ProductTable";
|
import ProductsTable from '@/components/pages/master-data/product/ProductTable';
|
||||||
|
|
||||||
const Product = () => {
|
const Product = () => {
|
||||||
return (
|
return (
|
||||||
<section className="w-full p-4">
|
<section className='w-full p-4'>
|
||||||
<ProductsTable />
|
<ProductsTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import SuppliersTable from "@/components/pages/master-data/supplier/SupplierTable";
|
import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable';
|
||||||
|
|
||||||
const Supplier = () => {
|
const Supplier = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
|
|
||||||
|
const AddProjectFlock = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<ProjectFlockForm formType='add' />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddProjectFlock;
|
||||||
@@ -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,60 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
export default function AddChickinKandang() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const projectFlockKandangId = searchParams.get('projectFlockKandangId');
|
||||||
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: projectFlockKandang,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshProjectFlockKandang,
|
||||||
|
} = useSWR(
|
||||||
|
`get-single-project-flock-kandang/${projectFlockKandangId}`,
|
||||||
|
async () =>
|
||||||
|
ProjectFlockKandangApi.getSingle(
|
||||||
|
parseInt(projectFlockKandangId as string)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!projectFlockKandangId) {
|
||||||
|
router.push(`/production/chickin/add?projectFlockId=${projectFlockId}`);
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && !projectFlockKandang) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAfterSubmit = () => {
|
||||||
|
refreshProjectFlockKandang();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading &&
|
||||||
|
isResponseSuccess(projectFlockKandang) &&
|
||||||
|
projectFlockId && (
|
||||||
|
<ChickinForm
|
||||||
|
initialValues={projectFlockKandang.data}
|
||||||
|
afterSubmit={handleAfterSubmit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,20 @@
|
|||||||
|
'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;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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,51 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const ProjectFlockEdit = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: projectFlock,
|
||||||
|
isLoading: isLoadingProjectFlock,
|
||||||
|
mutate: refreshProjectFlocks,
|
||||||
|
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
|
||||||
|
|
||||||
|
if (!projectFlockId) {
|
||||||
|
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))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-col justify-center'>
|
||||||
|
{isLoadingProjectFlock && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingProjectFlock && isResponseSuccess(projectFlock) && (
|
||||||
|
<ProjectFlockForm formType='edit' initialValues={projectFlock.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectFlockEdit;
|
||||||
@@ -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,55 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const ProjectFlockDetail = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: projectFlock,
|
||||||
|
isLoading: isLoadingProjectFlock,
|
||||||
|
mutate: refreshProjectFlock,
|
||||||
|
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
|
||||||
|
|
||||||
|
if (!projectFlockId) {
|
||||||
|
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))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-col justify-center'>
|
||||||
|
{isLoadingProjectFlock && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{isResponseSuccess(projectFlock) && (
|
||||||
|
<ProjectFlockForm
|
||||||
|
formType='detail'
|
||||||
|
initialValues={projectFlock.data}
|
||||||
|
refreshProjectFlocks={refreshProjectFlock}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectFlockDetail;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
|
||||||
|
|
||||||
|
const ProjectFlock = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<ProjectFlockTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectFlock;
|
||||||
@@ -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 RecordingForm from '@/components/pages/production/recording/form/RecordingForm';
|
||||||
|
|
||||||
|
const AddRecording = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<RecordingForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddRecording;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import RecordingForm from '@/components/pages/production/recording/form/RecordingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const RecordingEdit = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const recordingId = searchParams.get('recordingId');
|
||||||
|
|
||||||
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
|
recordingId,
|
||||||
|
(id: number) => RecordingApi.getSingle(id) // Gunakan RecordingApi
|
||||||
|
);
|
||||||
|
|
||||||
|
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 || isResponseError(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 && isResponseSuccess(recording) && (
|
||||||
|
<RecordingForm type='edit' initialValues={recording.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordingEdit;
|
||||||
@@ -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,47 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import RecordingForm from '@/components/pages/production/recording/form/RecordingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const RecordingDetail = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const recordingId = searchParams.get('recordingId');
|
||||||
|
|
||||||
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
|
recordingId,
|
||||||
|
(id: number) => RecordingApi.getSingle(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 || isResponseError(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 && isResponseSuccess(recording) && (
|
||||||
|
<RecordingForm type='detail' initialValues={recording.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordingDetail;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const AddGrading = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const recordingId = searchParams.get('recording_id');
|
||||||
|
|
||||||
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
|
recordingId && recordingId !== 'new' ? [recordingId] : null,
|
||||||
|
([id]) => RecordingApi.getSingle(parseInt(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
recordingId &&
|
||||||
|
recordingId !== 'new' &&
|
||||||
|
!isLoadingRecording &&
|
||||||
|
(!recording || !isResponseSuccess(recording))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{recordingId && recordingId !== 'new' && isLoadingRecording && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{(!recordingId ||
|
||||||
|
recordingId === 'new' ||
|
||||||
|
(!isLoadingRecording && recording && isResponseSuccess(recording))) && (
|
||||||
|
<GradingForm
|
||||||
|
type='add'
|
||||||
|
initialValues={
|
||||||
|
isResponseSuccess(recording) ? recording.data?.eggs?.[0] : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddGrading;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const EditGrading = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const recordingId = searchParams.get('recordingId');
|
||||||
|
const gradingId = searchParams.get('gradingId');
|
||||||
|
|
||||||
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
|
recordingId ? [recordingId] : null,
|
||||||
|
([id]) => RecordingApi.getSingle(parseInt(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!recordingId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingRecording && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingRecording && recording && isResponseSuccess(recording) && (
|
||||||
|
<GradingForm
|
||||||
|
type='edit'
|
||||||
|
initialValues={recording.data.eggs?.find(
|
||||||
|
(egg) => egg.id === parseInt(gradingId || '0')
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditGrading;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const DetailGrading = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const gradingId = searchParams.get('gradingId');
|
||||||
|
|
||||||
|
const { data: grading, isLoading: isLoadingGrading } = useSWR(
|
||||||
|
gradingId ? [gradingId] : null,
|
||||||
|
([id]) => RecordingApi.getSingle(parseInt(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gradingId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingGrading && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingGrading && grading && isResponseSuccess(grading) && (
|
||||||
|
<GradingForm
|
||||||
|
type='detail'
|
||||||
|
initialValues={grading.data.eggs?.find(
|
||||||
|
(egg) => egg.id === parseInt(gradingId)
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailGrading;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import RecordingTable from '@/components/pages/production/recording/RecordingTable';
|
||||||
|
|
||||||
|
const Recording = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<RecordingTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Recording;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import TransferToLayingForm from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm';
|
||||||
|
|
||||||
|
const AddTransferToLaying = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<TransferToLayingForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddTransferToLaying;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import TransferToLayingForm from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm';
|
||||||
|
|
||||||
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const TransferToLayingEdit = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const transferToLayingId = searchParams.get('transferToLayingId');
|
||||||
|
|
||||||
|
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
|
||||||
|
useSWR(transferToLayingId, (id: number) =>
|
||||||
|
TransferToLayingApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!transferToLayingId) {
|
||||||
|
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 (
|
||||||
|
!isLoadingTransferToLaying &&
|
||||||
|
(!transferToLaying || isResponseError(transferToLaying))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isResponseSuccess(transferToLaying) &&
|
||||||
|
transferToLaying.data.approval.step_number === 2
|
||||||
|
) {
|
||||||
|
router.replace('/production/transfer-to-laying');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingTransferToLaying && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
||||||
|
<TransferToLayingForm
|
||||||
|
type='edit'
|
||||||
|
initialValues={transferToLaying.data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransferToLayingEdit;
|
||||||
@@ -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 { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import TransferToLayingForm from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm';
|
||||||
|
|
||||||
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const TransferToLayingDetail = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const transferToLayingId = searchParams.get('transferToLayingId');
|
||||||
|
|
||||||
|
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
|
||||||
|
useSWR(transferToLayingId, (id: number) =>
|
||||||
|
TransferToLayingApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!transferToLayingId) {
|
||||||
|
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 (
|
||||||
|
!isLoadingTransferToLaying &&
|
||||||
|
(!transferToLaying || isResponseError(transferToLaying))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingTransferToLaying && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
|
||||||
|
<TransferToLayingForm
|
||||||
|
type='detail'
|
||||||
|
initialValues={transferToLaying.data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransferToLayingDetail;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable';
|
||||||
|
|
||||||
|
const TransferToLaying = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<TransferToLayingsTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransferToLaying;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import PurchaseRequestForm from '@/components/pages/purchase/form/request/PurchaseRequestForm';
|
||||||
|
|
||||||
|
const AddPurchaseRequest = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<PurchaseRequestForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddPurchaseRequest;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import PurchaseRequestForm from '@/components/pages/purchase/form/request/PurchaseRequestForm';
|
||||||
|
import { PurchaseApi } from '@/services/api/purchase';
|
||||||
|
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const PurchaseEdit = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const purchaseId = searchParams.get('purchaseId');
|
||||||
|
|
||||||
|
const { data: purchase, isLoading: isLoadingPurchase } = useSWR(
|
||||||
|
purchaseId,
|
||||||
|
(id: number) => PurchaseApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!purchaseId) {
|
||||||
|
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 (!isLoadingPurchase && (!purchase || isResponseError(purchase))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingPurchase && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingPurchase && isResponseSuccess(purchase) && (
|
||||||
|
<PurchaseRequestForm type='edit' initialValues={purchase.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PurchaseEdit;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import PurchaseOrderDetail from '@/components/pages/purchase/order/PurchaseOrderDetail';
|
||||||
|
import { PurchaseApi } from '@/services/api/purchase';
|
||||||
|
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const PurchaseDetail = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const purchaseId = searchParams.get('purchaseId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: purchase,
|
||||||
|
isLoading: isLoadingPurchase,
|
||||||
|
mutate: mutatePurchase,
|
||||||
|
} = useSWR(purchaseId, (id: number) => PurchaseApi.getSingle(id));
|
||||||
|
|
||||||
|
if (!purchaseId) {
|
||||||
|
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 (!isLoadingPurchase && (!purchase || isResponseError(purchase))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoadingPurchase && (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoadingPurchase && isResponseSuccess(purchase) && (
|
||||||
|
<PurchaseOrderDetail
|
||||||
|
type='detail'
|
||||||
|
initialValues={purchase.data}
|
||||||
|
refetchData={mutatePurchase}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PurchaseDetail;
|
||||||
@@ -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 PurchaseTable from '@/components/pages/purchase/PurchaseTable';
|
||||||
|
|
||||||
|
const Purchase = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<PurchaseTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Purchase;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLSpanElement>, 'className'> {
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: {
|
||||||
|
badge?: string;
|
||||||
|
};
|
||||||
|
variant?: 'default' | 'outline' | 'ghost' | 'soft' | 'dash';
|
||||||
|
color?:
|
||||||
|
| 'neutral'
|
||||||
|
| 'primary'
|
||||||
|
| 'secondary'
|
||||||
|
| 'accent'
|
||||||
|
| 'info'
|
||||||
|
| 'success'
|
||||||
|
| 'warning'
|
||||||
|
| 'error';
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Badge = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
color,
|
||||||
|
size = 'md',
|
||||||
|
...props
|
||||||
|
}: BadgeProps) => {
|
||||||
|
const getBadgeClasses = () => {
|
||||||
|
const baseClasses = 'badge';
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
default: '',
|
||||||
|
outline: 'badge-outline',
|
||||||
|
ghost: 'badge-ghost',
|
||||||
|
soft: 'badge-soft',
|
||||||
|
dash: 'badge-dash',
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorClasses = {
|
||||||
|
neutral: 'badge-neutral',
|
||||||
|
primary: 'badge-primary',
|
||||||
|
secondary: 'badge-secondary',
|
||||||
|
accent: 'badge-accent',
|
||||||
|
info: 'badge-info',
|
||||||
|
success: 'badge-success',
|
||||||
|
warning: 'badge-warning',
|
||||||
|
error: 'badge-error',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
xs: 'badge-xs',
|
||||||
|
sm: 'badge-sm',
|
||||||
|
md: 'badge-md',
|
||||||
|
lg: 'badge-lg',
|
||||||
|
xl: 'badge-xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
baseClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
color && colorClasses[color],
|
||||||
|
sizeClasses[size],
|
||||||
|
className?.badge
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={getBadgeClasses()} {...props}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Badge;
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import react from 'react';
|
import react from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
|
||||||
interface ButtonProps extends react.ComponentProps<'button'> {
|
export interface ButtonProps extends react.ComponentProps<'button'> {
|
||||||
variant?: 'soft' | 'outline' | 'dash' | 'ghost' | 'link' | 'active';
|
variant?: 'soft' | 'outline' | 'dash' | 'ghost' | 'link' | 'active';
|
||||||
color?: Color;
|
color?: Color;
|
||||||
href?: string;
|
href?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
target?: string;
|
||||||
|
rel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = ({
|
const Button = ({
|
||||||
@@ -22,6 +22,8 @@ const Button = ({
|
|||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
|
target,
|
||||||
|
rel,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) => {
|
}: ButtonProps) => {
|
||||||
const btnBaseClassName = cn(
|
const btnBaseClassName = cn(
|
||||||
@@ -43,7 +45,7 @@ const Button = ({
|
|||||||
'btn-warning': color === 'warning',
|
'btn-warning': color === 'warning',
|
||||||
'btn-error': color === 'error',
|
'btn-error': color === 'error',
|
||||||
},
|
},
|
||||||
'h-fit justify-center items-center gap-2 rounded-xl p-2 text-base transition-all'
|
'h-fit justify-center items-center gap-2 rounded p-2 text-base transition-all'
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -68,6 +70,8 @@ const Button = ({
|
|||||||
{href && (
|
{href && (
|
||||||
<Link
|
<Link
|
||||||
href={disabled ? '#' : href}
|
href={disabled ? '#' : href}
|
||||||
|
target={target}
|
||||||
|
rel={rel}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
btnBaseClassName,
|
btnBaseClassName,
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { HTMLAttributes, ReactNode, useState } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Collapse from './Collapse';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
export interface CardProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
image?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
imageWidth?: number;
|
||||||
|
imageHeight?: number;
|
||||||
|
actions?: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
collapsible?: boolean;
|
||||||
|
defaultCollapsed?: boolean;
|
||||||
|
onCollapsedChange?: (collapsed: boolean) => void;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
image?: string;
|
||||||
|
body?: string;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
actions?: string;
|
||||||
|
footer?: string;
|
||||||
|
collapsible?: string;
|
||||||
|
};
|
||||||
|
variant?: 'default' | 'compact' | 'bordered' | 'shadow' | 'image-full';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
image,
|
||||||
|
imageAlt,
|
||||||
|
imageWidth,
|
||||||
|
imageHeight,
|
||||||
|
actions,
|
||||||
|
footer,
|
||||||
|
collapsible,
|
||||||
|
defaultCollapsed = false,
|
||||||
|
onCollapsedChange,
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'md',
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CardProps) => {
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||||
|
|
||||||
|
const handleCollapsedChange = (open: boolean) => {
|
||||||
|
const collapsed = !open;
|
||||||
|
setIsCollapsed(collapsed);
|
||||||
|
onCollapsedChange?.(collapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCardClasses = () => {
|
||||||
|
const baseClasses = 'card bg-base-100';
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
default: '',
|
||||||
|
compact: 'card-compact',
|
||||||
|
bordered: 'border border-base-300',
|
||||||
|
shadow: 'shadow-xl',
|
||||||
|
'image-full': 'card-side card-compact shadow-xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'w-64',
|
||||||
|
md: 'w-96',
|
||||||
|
lg: 'w-[28rem]',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
baseClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
variant !== 'image-full' ? sizeClasses[size] : '',
|
||||||
|
className?.wrapper
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageDimensions = () => {
|
||||||
|
if (variant === 'image-full') {
|
||||||
|
return {
|
||||||
|
width: imageWidth || 128,
|
||||||
|
height: imageHeight || 128,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardWidths = {
|
||||||
|
sm: 256, // w-64
|
||||||
|
md: 384, // w-96
|
||||||
|
lg: 448, // w-[28rem]
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: imageWidth || cardWidths[size],
|
||||||
|
height: imageHeight || 192,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageClasses = () => {
|
||||||
|
if (variant === 'image-full') {
|
||||||
|
return cn('object-cover', className?.image);
|
||||||
|
}
|
||||||
|
return cn('w-full object-cover', className?.image);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBodyClasses = () => {
|
||||||
|
const baseClasses = 'card-body';
|
||||||
|
|
||||||
|
if (variant === 'compact' || variant === 'image-full') {
|
||||||
|
return cn(baseClasses, 'p-4', className?.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cn(baseClasses, 'p-6', className?.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitleClasses = () => {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'text-lg',
|
||||||
|
md: 'text-xl',
|
||||||
|
lg: 'text-2xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn('card-title font-bold', sizeClasses[size], className?.title);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubtitleClasses = () => {
|
||||||
|
return cn('text-base-content/70 text-sm mt-1', className?.subtitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionsClasses = () => {
|
||||||
|
return cn('card-actions justify-end mt-4', className?.actions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFooterClasses = () => {
|
||||||
|
return cn('border-t border-base-300 mt-4 pt-4', className?.footer);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCardContent = () => {
|
||||||
|
const hasContent = children || actions || footer;
|
||||||
|
|
||||||
|
const titleContent = (
|
||||||
|
<div className='group flex items-center !justify-between w-full'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
{title && <h2 className={getTitleClasses()}>{title}</h2>}
|
||||||
|
{subtitle && <p className={getSubtitleClasses()}>{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
{collapsible && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleCollapsedChange(!isCollapsed)}
|
||||||
|
className='btn btn-ghost btn-sm btn-circle'
|
||||||
|
aria-label={isCollapsed ? 'Expand content' : 'Collapse content'}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
isCollapsed
|
||||||
|
? 'material-symbols:expand-more'
|
||||||
|
: 'material-symbols:expand-less'
|
||||||
|
}
|
||||||
|
width={20}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const cardContent = (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{children}
|
||||||
|
{actions && <div className={getActionsClasses()}>{actions}</div>}
|
||||||
|
{footer && <div className={getFooterClasses()}>{footer}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{image && (
|
||||||
|
<figure>
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
alt={imageAlt || title || 'Card image'}
|
||||||
|
width={getImageDimensions().width}
|
||||||
|
height={getImageDimensions().height}
|
||||||
|
className={getImageClasses()}
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
)}
|
||||||
|
<div className={getBodyClasses()}>
|
||||||
|
{collapsible && hasContent ? (
|
||||||
|
<Collapse
|
||||||
|
variant='default'
|
||||||
|
bordered={false}
|
||||||
|
open={!isCollapsed}
|
||||||
|
onOpenChange={handleCollapsedChange}
|
||||||
|
title={titleContent}
|
||||||
|
titleClassName='w-full cursor-pointer'
|
||||||
|
contentClassName='p-0'
|
||||||
|
fullWidth={true}
|
||||||
|
>
|
||||||
|
{cardContent}
|
||||||
|
</Collapse>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{(title || subtitle) && (
|
||||||
|
<div className='mb-4'>
|
||||||
|
{title && <h2 className={getTitleClasses()}>{title}</h2>}
|
||||||
|
{subtitle && (
|
||||||
|
<p className={getSubtitleClasses()}>{subtitle}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasContent && cardContent}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (variant === 'image-full' && image) {
|
||||||
|
return (
|
||||||
|
<div className={getCardClasses()} {...props}>
|
||||||
|
{renderCardContent()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={getCardClasses()} {...props}>
|
||||||
|
{renderCardContent()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Card;
|
||||||
@@ -26,6 +26,9 @@ export type CollapseProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
/** Allow only one open at a time by switching to radio input */
|
/** Allow only one open at a time by switching to radio input */
|
||||||
asRadio?: boolean;
|
asRadio?: boolean;
|
||||||
|
/** Force full width instead of auto-fit when collapsed
|
||||||
|
* (Khusus justify-between dan justify-end) */
|
||||||
|
fullWidth?: boolean;
|
||||||
/** Extra classnames */
|
/** Extra classnames */
|
||||||
className?: string;
|
className?: string;
|
||||||
titleClassName?: string;
|
titleClassName?: string;
|
||||||
@@ -44,6 +47,7 @@ export const Collapse = ({
|
|||||||
bordered,
|
bordered,
|
||||||
disabled,
|
disabled,
|
||||||
asRadio = false,
|
asRadio = false,
|
||||||
|
fullWidth,
|
||||||
className,
|
className,
|
||||||
titleClassName,
|
titleClassName,
|
||||||
contentClassName,
|
contentClassName,
|
||||||
@@ -70,7 +74,7 @@ export const Collapse = ({
|
|||||||
variant === 'plus' && 'collapse-plus',
|
variant === 'plus' && 'collapse-plus',
|
||||||
bordered && 'border base-content/20 border-opacity-20 rounded-box',
|
bordered && 'border base-content/20 border-opacity-20 rounded-box',
|
||||||
disabled && 'opacity-60 pointer-events-none',
|
disabled && 'opacity-60 pointer-events-none',
|
||||||
!open && 'w-fit',
|
!fullWidth && !open && 'w-fit',
|
||||||
className
|
className
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+30
-148
@@ -1,163 +1,30 @@
|
|||||||
'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 SidebarMenu from '@/components/molecules/SidebarMenu';
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
type CollapseMenuProps = {
|
|
||||||
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 closeMainDrawerHandler = () => {
|
||||||
|
setMainDrawerOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-col gap-4'>
|
<div className='w-full p-4 flex flex-col gap-4'>
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex flex-row items-center gap-4'>
|
||||||
<Image
|
<Image
|
||||||
src='/assets/img/lti-logo.png'
|
src='/assets/img/lti-logo.png'
|
||||||
alt='MBU Logo'
|
alt='MBU Logo'
|
||||||
@@ -167,9 +34,24 @@ const MainDrawerContent = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<h1 className='text-xl font-bold'>LTI ERP</h1>
|
<h1 className='text-xl font-bold'>LTI ERP</h1>
|
||||||
|
|
||||||
|
<div className='grow flex flex-row justify-end sm:hidden'>
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
color='error'
|
||||||
|
onClick={closeMainDrawerHandler}
|
||||||
|
className='rounded-full'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:close-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MainDrawerMenu />
|
<SidebarMenu menu={MAIN_DRAWER_LINKS} activeLink={pathname} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -194,9 +76,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;
|
||||||
|
|||||||
+43
-25
@@ -1,38 +1,52 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
|
import {
|
||||||
|
ReactNode,
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
export const useModal = () => {
|
export const useModal = (isNestingModal = false) => {
|
||||||
const ref = useRef<HTMLDialogElement>(null);
|
const ref = useRef<HTMLDialogElement>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const openModal = useCallback(() => {
|
const openModal = useCallback(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
if (isNestingModal) {
|
||||||
|
ref.current.showModal();
|
||||||
|
} else {
|
||||||
|
ref.current.show();
|
||||||
|
}
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
|
}, [isNestingModal]);
|
||||||
ref.current?.showModal();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
const closeModal = useCallback(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
ref.current.close();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
ref.current?.close();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
if (open) {
|
open ? closeModal() : openModal();
|
||||||
closeModal();
|
|
||||||
} else {
|
|
||||||
openModal();
|
|
||||||
}
|
|
||||||
}, [open, closeModal, openModal]);
|
}, [open, closeModal, openModal]);
|
||||||
|
|
||||||
if (ref.current) {
|
useEffect(() => {
|
||||||
ref.current.addEventListener('close', () => {
|
const dialog = ref.current;
|
||||||
closeModal();
|
if (!dialog) return;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ref, open, setOpen, openModal, closeModal, toggle } as const;
|
const handleClose = () => setOpen(false);
|
||||||
|
dialog.addEventListener('close', handleClose);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dialog.removeEventListener('close', handleClose);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { ref, open, openModal, closeModal, toggle } as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
@@ -46,15 +60,19 @@ interface ModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
|
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
|
||||||
return (
|
const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
||||||
<dialog ref={ref} className={cn('modal', className?.modal)}>
|
if (closeOnBackdrop && e.target === ref.current) {
|
||||||
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
|
ref.current?.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
{closeOnBackdrop && (
|
return (
|
||||||
<form method='dialog' className='modal-backdrop'>
|
<dialog
|
||||||
<button>close</button>
|
ref={ref}
|
||||||
</form>
|
className={cn('modal', className?.modal)}
|
||||||
)}
|
onClick={handleBackdropClick}
|
||||||
|
>
|
||||||
|
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,38 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
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 { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
import { AuthApi } from '@/services/api/auth';
|
||||||
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
title: string;
|
title: string;
|
||||||
toggleSidebar?: () => void;
|
toggleSidebar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
||||||
|
const { setUser } = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const logoutClickHandler = async () => {
|
||||||
|
const logoutRes = await AuthApi.logout();
|
||||||
|
|
||||||
|
if (isResponseError(logoutRes)) {
|
||||||
|
toast.error('Gagal logout! Coba lagi!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser(undefined);
|
||||||
|
router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='navbar px-4 bg-base-100 shadow-sm'>
|
<div className='navbar px-4 bg-base-100 shadow-sm'>
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
@@ -42,8 +64,7 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Menu className='dropdown-content w-52 mt-3 p-2 bg-base-100 shadow rounded-box menu-sm'>
|
<Menu className='dropdown-content w-52 mt-3 p-2 bg-base-100 shadow rounded-box menu-sm'>
|
||||||
<MenuItem title='Settings' href='#' />
|
<MenuItem title='Logout' onClick={logoutClickHandler} />
|
||||||
<MenuItem title='Logout' href='#' />
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+299
-209
@@ -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 = ({
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
const rowChangeHandler: ChangeEventHandler<HTMLSelectElement> = (e) => {
|
||||||
|
onRowChange?.(Number(e.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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}
|
||||||
|
onClick={firstPageClickHandler}
|
||||||
|
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-double-left'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-gray-400 group-disabled:text-gray-300'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PrevPageButton = () => (
|
||||||
|
<Button
|
||||||
|
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 (
|
return (
|
||||||
<div>
|
<div className='@container'>
|
||||||
<div className='join w-full justify-between items-center gap-3'>
|
<div className='flex flex-row justify-center items-center'>
|
||||||
<button
|
<div className='hidden @lg:block'>
|
||||||
disabled={currentPage === 1}
|
<DisplayedRowCountSelect />
|
||||||
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 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'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='uil:arrow-left'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className='text-gray-400 group-disabled:text-gray-300'
|
|
||||||
/>{' '}
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{totalPages <= 7 && (
|
<div className='join w-full justify-end @lg:justify-center items-center gap-0.5'>
|
||||||
<div className='join-item join gap-0.5'>
|
<div className='hidden @lg:block'>
|
||||||
{range(1, totalPages).map((pageNumber) => (
|
<GoToFirstPageButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='hidden @lg:block'>
|
||||||
|
<PrevPageButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalPages <= 7 &&
|
||||||
|
range(1, totalPages).map((pageNumber) => (
|
||||||
<PaginationButton
|
<PaginationButton
|
||||||
key={pageNumber}
|
key={pageNumber}
|
||||||
content={pageNumber}
|
content={pageNumber}
|
||||||
@@ -138,195 +255,168 @@ const Pagination = ({
|
|||||||
onClick={() => pageChangeHandler(pageNumber)}
|
onClick={() => pageChangeHandler(pageNumber)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages > 7 && (
|
{totalPages > 7 && (
|
||||||
<div className='join-item join gap-0.5'>
|
<>
|
||||||
<PaginationButton
|
|
||||||
content={1}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
onClick={() => pageChangeHandler(1)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{totalPages >= 2 &&
|
|
||||||
(currentPage <= 3 || currentPage >= totalPages - 2) && (
|
|
||||||
<PaginationButton
|
|
||||||
content={2}
|
|
||||||
disabled={currentPage === 2}
|
|
||||||
onClick={() => pageChangeHandler(2)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 2 &&
|
|
||||||
currentPage > 3 &&
|
|
||||||
currentPage < totalPages - 2 && (
|
|
||||||
<EtcPaginationButton
|
|
||||||
startPage={2}
|
|
||||||
endPage={currentPage - 2}
|
|
||||||
onPageItemClick={pageChangeHandler}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 3 &&
|
|
||||||
(currentPage <= 4 || currentPage >= totalPages - 2) &&
|
|
||||||
currentPage !== totalPages - 2 && (
|
|
||||||
<PaginationButton
|
|
||||||
content={3}
|
|
||||||
disabled={currentPage === 3}
|
|
||||||
onClick={() => pageChangeHandler(3)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 7 &&
|
|
||||||
(currentPage <= 2 || currentPage >= totalPages - 2) && (
|
|
||||||
<EtcPaginationButton
|
|
||||||
startPage={
|
|
||||||
currentPage <= 2
|
|
||||||
? currentPage + 2
|
|
||||||
: currentPage === totalPages - 2
|
|
||||||
? 3
|
|
||||||
: currentPage >= totalPages - 1
|
|
||||||
? 4
|
|
||||||
: 1
|
|
||||||
}
|
|
||||||
endPage={
|
|
||||||
currentPage <= 2 || currentPage >= totalPages - 1
|
|
||||||
? totalPages - 3
|
|
||||||
: currentPage === totalPages - 2
|
|
||||||
? totalPages - 4
|
|
||||||
: 2
|
|
||||||
}
|
|
||||||
onPageItemClick={pageChangeHandler}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 3 &&
|
|
||||||
currentPage > 4 &&
|
|
||||||
currentPage < totalPages - 1 && (
|
|
||||||
<PaginationButton
|
|
||||||
content={currentPage - 1}
|
|
||||||
onClick={() => pageChangeHandler(currentPage - 1)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 7 &&
|
|
||||||
currentPage > 3 &&
|
|
||||||
currentPage < totalPages - 2 && (
|
|
||||||
<PaginationButton content={currentPage} disabled />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 5 &&
|
|
||||||
currentPage > 2 &&
|
|
||||||
currentPage < totalPages - 2 && (
|
|
||||||
<PaginationButton
|
|
||||||
content={currentPage + 1}
|
|
||||||
onClick={() => pageChangeHandler(currentPage + 1)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 5 &&
|
|
||||||
(currentPage <= 2 || currentPage >= totalPages - 2) && (
|
|
||||||
<PaginationButton
|
|
||||||
content={totalPages - 2}
|
|
||||||
disabled={currentPage === totalPages - 2}
|
|
||||||
onClick={() => pageChangeHandler(totalPages - 2)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 6 &&
|
|
||||||
currentPage > 2 &&
|
|
||||||
currentPage < totalPages - 3 && (
|
|
||||||
<EtcPaginationButton
|
|
||||||
startPage={
|
|
||||||
currentPage <= 3
|
|
||||||
? currentPage + 2
|
|
||||||
: currentPage >= 4
|
|
||||||
? currentPage + 2
|
|
||||||
: 1
|
|
||||||
}
|
|
||||||
endPage={
|
|
||||||
currentPage <= 3
|
|
||||||
? totalPages - 2
|
|
||||||
: currentPage >= 4
|
|
||||||
? totalPages - 1
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
onPageItemClick={pageChangeHandler}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 6 &&
|
|
||||||
(currentPage <= 3 || currentPage >= totalPages - 3) && (
|
|
||||||
<PaginationButton
|
|
||||||
content={totalPages - 1}
|
|
||||||
disabled={currentPage === totalPages - 1}
|
|
||||||
onClick={() => pageChangeHandler(totalPages - 1)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{totalPages >= 7 && (
|
|
||||||
<PaginationButton
|
<PaginationButton
|
||||||
content={totalPages}
|
content={1}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === 1}
|
||||||
onClick={() => pageChangeHandler(totalPages)}
|
onClick={() => pageChangeHandler(1)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
{totalPages >= 2 &&
|
||||||
disabled={currentPage === totalPages}
|
(currentPage <= 3 || currentPage >= totalPages - 2) && (
|
||||||
onClick={onNextPage}
|
<PaginationButton
|
||||||
className={cn(
|
content={2}
|
||||||
'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={currentPage === 2}
|
||||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
onClick={() => pageChangeHandler(2)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 2 &&
|
||||||
|
currentPage > 3 &&
|
||||||
|
currentPage < totalPages - 2 && (
|
||||||
|
<EtcPaginationButton
|
||||||
|
startPage={2}
|
||||||
|
endPage={currentPage - 2}
|
||||||
|
onPageItemClick={pageChangeHandler}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 3 &&
|
||||||
|
(currentPage <= 4 || currentPage >= totalPages - 2) &&
|
||||||
|
currentPage !== totalPages - 2 && (
|
||||||
|
<PaginationButton
|
||||||
|
content={3}
|
||||||
|
disabled={currentPage === 3}
|
||||||
|
onClick={() => pageChangeHandler(3)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 7 &&
|
||||||
|
(currentPage <= 2 || currentPage >= totalPages - 2) && (
|
||||||
|
<EtcPaginationButton
|
||||||
|
startPage={
|
||||||
|
currentPage <= 2
|
||||||
|
? currentPage + 2
|
||||||
|
: currentPage === totalPages - 2
|
||||||
|
? 3
|
||||||
|
: currentPage >= totalPages - 1
|
||||||
|
? 4
|
||||||
|
: 1
|
||||||
|
}
|
||||||
|
endPage={
|
||||||
|
currentPage <= 2 || currentPage >= totalPages - 1
|
||||||
|
? totalPages - 3
|
||||||
|
: currentPage === totalPages - 2
|
||||||
|
? totalPages - 4
|
||||||
|
: 2
|
||||||
|
}
|
||||||
|
onPageItemClick={pageChangeHandler}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 3 &&
|
||||||
|
currentPage > 4 &&
|
||||||
|
currentPage < totalPages - 1 && (
|
||||||
|
<PaginationButton
|
||||||
|
content={currentPage - 1}
|
||||||
|
onClick={() => pageChangeHandler(currentPage - 1)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 7 &&
|
||||||
|
currentPage > 3 &&
|
||||||
|
currentPage < totalPages - 2 && (
|
||||||
|
<PaginationButton content={currentPage} disabled />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 5 &&
|
||||||
|
currentPage > 2 &&
|
||||||
|
currentPage < totalPages - 2 && (
|
||||||
|
<PaginationButton
|
||||||
|
content={currentPage + 1}
|
||||||
|
onClick={() => pageChangeHandler(currentPage + 1)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 5 &&
|
||||||
|
(currentPage <= 2 || currentPage >= totalPages - 2) && (
|
||||||
|
<PaginationButton
|
||||||
|
content={totalPages - 2}
|
||||||
|
disabled={currentPage === totalPages - 2}
|
||||||
|
onClick={() => pageChangeHandler(totalPages - 2)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 6 &&
|
||||||
|
currentPage > 2 &&
|
||||||
|
currentPage < totalPages - 3 && (
|
||||||
|
<EtcPaginationButton
|
||||||
|
startPage={
|
||||||
|
currentPage <= 3
|
||||||
|
? currentPage + 2
|
||||||
|
: currentPage >= 4
|
||||||
|
? currentPage + 2
|
||||||
|
: 1
|
||||||
|
}
|
||||||
|
endPage={
|
||||||
|
currentPage <= 3
|
||||||
|
? totalPages - 2
|
||||||
|
: currentPage >= 4
|
||||||
|
? totalPages - 1
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageItemClick={pageChangeHandler}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 6 &&
|
||||||
|
(currentPage <= 3 || currentPage >= totalPages - 3) && (
|
||||||
|
<PaginationButton
|
||||||
|
content={totalPages - 1}
|
||||||
|
disabled={currentPage === totalPages - 1}
|
||||||
|
onClick={() => pageChangeHandler(totalPages - 1)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages >= 7 && (
|
||||||
|
<PaginationButton
|
||||||
|
content={totalPages}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
onClick={() => pageChangeHandler(totalPages)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
Next{' '}
|
<div className='hidden @lg:block'>
|
||||||
<Icon
|
<NextPageButton />
|
||||||
icon='uil:arrow-right'
|
</div>
|
||||||
width={20}
|
|
||||||
height={20}
|
<div className='hidden @lg:block'>
|
||||||
className='text-gray-400 group-disabled:text-gray-300'
|
<GoToLastPageButton />
|
||||||
/>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
|
<div className='hidden @lg:block'>
|
||||||
|
<PageInfo />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-2 mt-2 sm:hidden'>
|
<div className='flex @lg:hidden flex-col justify-center items-end gap-2'>
|
||||||
<button
|
<div className='flex flex-row items-center gap-0.5'>
|
||||||
disabled={currentPage === 1}
|
<GoToFirstPageButton />
|
||||||
onClick={onPrevPage}
|
<PrevPageButton />
|
||||||
className={cn(
|
<NextPageButton />
|
||||||
'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',
|
<GoToLastPageButton />
|
||||||
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
|
</div>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='uil:arrow-left'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className='text-gray-400 group-disabled:text-gray-300'
|
|
||||||
/>{' '}
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<div className='flex flex-row items-center gap-4'>
|
||||||
disabled={currentPage === totalPages}
|
<DisplayedRowCountSelect />
|
||||||
onClick={onNextPage}
|
|
||||||
className={cn(
|
<PageInfo />
|
||||||
'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',
|
</div>
|
||||||
'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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
interface PillBadgeProps {
|
||||||
|
content: ReactNode;
|
||||||
|
color?: 'yellow' | 'blue' | 'green' | 'red' | 'purple' | 'gray';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PillBadge = ({ content, color = 'gray', className }: PillBadgeProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-fit min-w-max px-2 py-0.5 flex justify-center items-center gap-1 rounded-full border border-gray-200 bg-gray-50 text-gray-500 drop-shadow-xs capitalize',
|
||||||
|
{
|
||||||
|
'border-yellow-200 bg-yellow-50 text-yellow-500': color === 'yellow',
|
||||||
|
'border-blue-200 bg-blue-50 text-blue-500': color === 'blue',
|
||||||
|
'border-green-200 bg-green-50 text-green-500': color === 'green',
|
||||||
|
'border-red-200 bg-red-50 text-red-500': color === 'red',
|
||||||
|
'border-purple-200 bg-purple-50 text-purple-500': color === 'purple',
|
||||||
|
'border-neutral-200 bg-neutral-50 text-neutral-500': color === 'gray',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PillBadge;
|
||||||
+79
-29
@@ -13,6 +13,7 @@ import {
|
|||||||
FilterFn,
|
FilterFn,
|
||||||
SortingState,
|
SortingState,
|
||||||
OnChangeFn,
|
OnChangeFn,
|
||||||
|
Row,
|
||||||
} 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';
|
||||||
@@ -37,6 +38,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;
|
||||||
@@ -48,6 +50,11 @@ export interface TableProps<TData extends object> {
|
|||||||
sorting?: SortingState;
|
sorting?: SortingState;
|
||||||
setSorting?: OnChangeFn<SortingState>;
|
setSorting?: OnChangeFn<SortingState>;
|
||||||
manualSorting?: boolean;
|
manualSorting?: boolean;
|
||||||
|
rowSelection?: Record<string, boolean>;
|
||||||
|
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
||||||
|
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
||||||
|
withCheckbox?: boolean;
|
||||||
|
rowOptions?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
||||||
@@ -60,38 +67,52 @@ const emptyContentDefaultValue = (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 text-base-content/50',
|
||||||
|
tableBodyClassName: '',
|
||||||
|
bodyRowClassName: 'border-t border-t-base-content/10',
|
||||||
|
bodyColumnClassName: 'px-4 py-3 text-base-content',
|
||||||
|
paginationClassName: '',
|
||||||
|
};
|
||||||
|
|
||||||
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,
|
||||||
manualSorting = false,
|
manualSorting = false,
|
||||||
|
rowSelection,
|
||||||
|
setRowSelection,
|
||||||
|
enableRowSelection,
|
||||||
|
withCheckbox = false,
|
||||||
|
rowOptions = [10, 20, 50, 100],
|
||||||
}: 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,
|
||||||
@@ -137,6 +158,19 @@ const Table = <TData extends object>({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rowSelection && setRowSelection) {
|
||||||
|
tableOptions.onRowSelectionChange = setRowSelection;
|
||||||
|
tableOptions.state = {
|
||||||
|
...tableOptions.state,
|
||||||
|
rowSelection,
|
||||||
|
};
|
||||||
|
tableOptions.getRowId = (row) => (row as { id: string }).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableRowSelection !== undefined) {
|
||||||
|
tableOptions.enableRowSelection = enableRowSelection;
|
||||||
|
}
|
||||||
|
|
||||||
const table = useReactTable(tableOptions);
|
const table = useReactTable(tableOptions);
|
||||||
const { setPageSize } = table;
|
const { setPageSize } = table;
|
||||||
|
|
||||||
@@ -171,12 +205,15 @@ 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
|
||||||
|
key={headerGroup.id}
|
||||||
|
className={tableClassNames.headerRowClassName}
|
||||||
|
>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th
|
<th
|
||||||
key={header.id}
|
key={header.id}
|
||||||
@@ -186,7 +223,10 @@ const Table = <TData extends object>({
|
|||||||
header.column.getCanSort()
|
header.column.getCanSort()
|
||||||
? 'cursor-pointer select-none'
|
? 'cursor-pointer select-none'
|
||||||
: '',
|
: '',
|
||||||
className.headerColumnClassName
|
{
|
||||||
|
'first:w-9 first:pr-0': withCheckbox,
|
||||||
|
},
|
||||||
|
tableClassNames.headerColumnClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-1'>
|
<div className='flex items-center gap-1'>
|
||||||
@@ -196,12 +236,13 @@ const Table = <TData extends object>({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{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'
|
||||||
@@ -209,10 +250,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'
|
||||||
@@ -228,11 +270,17 @@ const Table = <TData extends object>({
|
|||||||
))}
|
))}
|
||||||
</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}>
|
<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())}
|
||||||
|
|
||||||
@@ -250,7 +298,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}
|
||||||
@@ -262,6 +310,8 @@ const Table = <TData extends object>({
|
|||||||
onPrevPage={prevPageClickHandler}
|
onPrevPage={prevPageClickHandler}
|
||||||
onNextPage={nextPageClickHandler}
|
onNextPage={nextPageClickHandler}
|
||||||
onPageChange={pageChangeHandler}
|
onPageChange={pageChangeHandler}
|
||||||
|
rowOptions={rowOptions}
|
||||||
|
onRowChange={onPageSizeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export interface TabItem {
|
||||||
|
id: string;
|
||||||
|
label: ReactNode;
|
||||||
|
content?: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
|
||||||
|
tabs: TabItem[];
|
||||||
|
variant?: 'bordered' | 'lifted' | 'boxed';
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
placement?: 'top' | 'bottom';
|
||||||
|
/** Tab yang aktif secara default (uncontrolled mode) */
|
||||||
|
defaultActiveId?: string;
|
||||||
|
/** Tab yang aktif (controlled mode, dikontrol parent) */
|
||||||
|
activeTabId?: string;
|
||||||
|
className?:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
wrapper?: string;
|
||||||
|
tab?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
onTabChange?: (tabId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tabs = ({
|
||||||
|
tabs,
|
||||||
|
variant,
|
||||||
|
size = 'md',
|
||||||
|
placement = 'top',
|
||||||
|
defaultActiveId,
|
||||||
|
activeTabId: controlledActiveId,
|
||||||
|
className,
|
||||||
|
onTabChange,
|
||||||
|
...props
|
||||||
|
}: TabsProps) => {
|
||||||
|
// State internal hanya dipakai kalau `activeTabId` (controlled) tidak diset
|
||||||
|
const [uncontrolledActiveId, setUncontrolledActiveId] = useState(
|
||||||
|
defaultActiveId || tabs[0]?.id || ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const isControlled = controlledActiveId !== undefined;
|
||||||
|
const activeTabId = isControlled ? controlledActiveId : uncontrolledActiveId;
|
||||||
|
|
||||||
|
const handleTabChange = (tabId: string) => {
|
||||||
|
if (tabId === activeTabId) return;
|
||||||
|
if (!isControlled) setUncontrolledActiveId(tabId);
|
||||||
|
onTabChange?.(tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { wrapper: wrapperClassName, tab: tabClassName } =
|
||||||
|
typeof className === 'object'
|
||||||
|
? className
|
||||||
|
: { wrapper: className, tab: undefined };
|
||||||
|
|
||||||
|
const getTabsClasses = () => {
|
||||||
|
const variantClasses: Record<string, string> = {
|
||||||
|
bordered: 'tabs-bordered',
|
||||||
|
lifted: 'tabs-lift',
|
||||||
|
boxed: 'tabs-box',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses: Record<string, string> = {
|
||||||
|
xs: 'tabs-xs',
|
||||||
|
sm: 'tabs-sm',
|
||||||
|
md: '',
|
||||||
|
lg: 'tabs-lg',
|
||||||
|
xl: 'tabs-xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
const placementClasses: Record<string, string> = {
|
||||||
|
top: '',
|
||||||
|
bottom: 'tabs-bottom',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
'tabs',
|
||||||
|
variant && variantClasses[variant],
|
||||||
|
sizeClasses[size],
|
||||||
|
placementClasses[placement],
|
||||||
|
wrapperClassName
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabClasses = (isActive: boolean, isDisabled?: boolean) =>
|
||||||
|
cn(
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
'tab-active': isActive,
|
||||||
|
'tab-disabled': isDisabled,
|
||||||
|
},
|
||||||
|
tabClassName
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeContent = tabs.find((tab) => tab.id === activeTabId)?.content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'w-full',
|
||||||
|
typeof className === 'string' ? className : undefined
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div role='tablist' className={getTabsClasses()}>
|
||||||
|
{tabs.map(({ id, label, disabled }) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
role='tab'
|
||||||
|
className={getTabClasses(id === activeTabId, disabled)}
|
||||||
|
onClick={() => !disabled && handleTabChange(id)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeContent && <div className='mt-4'>{activeContent}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabs;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import { Color } from '@/types/theme';
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
content?: ReactNode;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
open?: boolean;
|
||||||
|
color?: Color;
|
||||||
|
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tooltip = ({
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
className,
|
||||||
|
open,
|
||||||
|
color,
|
||||||
|
position,
|
||||||
|
}: TooltipProps) => {
|
||||||
|
const tooltipBaseClassName = cn('tooltip', {
|
||||||
|
'tooltip-open': typeof open === 'boolean' && open,
|
||||||
|
|
||||||
|
'tooltip-top': position === 'top',
|
||||||
|
'tooltip-bottom': position === 'bottom',
|
||||||
|
'tooltip-left': position === 'left',
|
||||||
|
'tooltip-right': position === 'right',
|
||||||
|
|
||||||
|
'tooltip-primary': color === 'primary',
|
||||||
|
'tooltip-secondary': color === 'secondary',
|
||||||
|
'tooltip-accent': color === 'accent',
|
||||||
|
'tooltip-neutral': color === 'neutral',
|
||||||
|
'tooltip-info': color === 'info',
|
||||||
|
'tooltip-success': color === 'success',
|
||||||
|
'tooltip-warning': color === 'warning',
|
||||||
|
'tooltip-error': color === 'error',
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className={cn(tooltipBaseClassName, className?.wrapper)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'tooltip-content',
|
||||||
|
'max-w-60 sm:max-w-xs',
|
||||||
|
className?.content
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tooltip;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user