Compare commits

..

196 Commits

Author SHA1 Message Date
Adnan Zahir 212fd3b4f2 Merge branch 'feat/FE/US-34/stock-adjustment' into 'development'
[FEAR/FE/US#34/TASK#51-52-53-54] Implement Feature Adjustment Inventory

See merge request mbugroup/lti-web-client!9
2025-10-15 10:26:14 +07:00
Rivaldi A N S 46860a93b9 Merge branch 'dev/randy' into 'feat/FE/US-34/stock-adjustment'
[FEAT/FE][US#33][TASK#51-54] Form Validation and UI/UX Adjustment

See merge request mbugroup/lti-web-client!10
2025-10-13 06:53:00 +00:00
randy-ar 302da65c59 fix(FE-51) adjust textarea component ui 2025-10-13 13:09:53 +07:00
randy-ar ce8471343c fix(FE-54) fix form input state inventory adjustment 2025-10-13 12:59:05 +07:00
randy-ar 880ff5740d fix(FE-42): fix validation supplier form and multi select component 2025-10-13 11:26:37 +07:00
randy-ar 9b53c75f2f fix(FE-42): fix validation supplier form and multi select component 2025-10-13 11:25:39 +07:00
randy-ar f662f2951e fix(FE-51-54): fixing bug and layout form adjustment 2025-10-11 23:26:30 +07:00
randy-ar 1fd4b2aba5 Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into feat/FE/US-34/stock-adjustment 2025-10-11 13:18:15 +07:00
randy-ar b75b5956eb feat/FE/US-34/TASK-52-53-slicing-ui-table-adjust-form-with-api 2025-10-11 13:12:05 +07:00
randy-ar aa7b6581d9 feat/FE/US-34/TASK-54-51-slicing-ui-client-side-validation-stock-adjustment 2025-10-11 02:03:10 +07:00
Adnan Zahir 24b702548d Merge branch 'feat/FE/US-33/master-data-management' into 'development'
[FEAT/FE][US#33/TASK#40-41-42-43] Master Data Management

See merge request mbugroup/lti-web-client!8
2025-10-09 17:43:27 +07:00
Rivaldi A N S 96babba4bb Merge branch 'feat/FE/US-33/TASK-40-slicing-ui-for-master-data-customers-and-suppliers-forms' into 'feat/FE/US-33/TASK-40-slicing-ui-for-master-data-forms'
[FEAT/FE/US#33/TASK#40] Slicing UI for Costumer and Suppliers Forms in Master Data

See merge request mbugroup/lti-web-client!7
2025-10-09 08:19:28 +00:00
Rivaldi A N S acd66b0323 Merge branch 'bugfix/FE/ISSUE/build-error' into 'feat/FE/US-33/TASK-40-slicing-ui-for-master-data-forms'
[FIX/FE][ISSUE] Fix Issue for Next.js Build

See merge request mbugroup/lti-web-client!4
2025-10-09 07:50:58 +00:00
randy-ar 5cf98ed95e fix(FE-33): fix conflict git 2025-10-09 13:35:30 +07:00
randy-ar a83452a0e4 feat(FE-33): create suppliers table and forms 2025-10-09 12:27:59 +07:00
rstubryan 3f712a638f chore: remove redundant start scripts from package.json 2025-10-09 11:45:27 +07:00
rstubryan 0ea78fee92 chore: resolve conflict pull request 2025-10-09 11:44:38 +07:00
rstubryan b561ed6193 fix: update dependencies in multiple tables to include updateSortingFilter in effect dependencies 2025-10-09 11:35:22 +07:00
rstubryan d7ae7e00d3 fix: update dependency in AreasTable to include updateFilter in sorting effect 2025-10-09 11:22:01 +07:00
rstubryan f0eabedcb2 refactor: refactor layout components to use SuspenseHelper for loading states 2025-10-09 11:06:59 +07:00
ValdiANS 94a5ce5604 feat(FE-40): create layout for /detail route and wrap the children in SuspenseHelper component 2025-10-09 11:05:55 +07:00
ValdiANS 9b56308cf0 feat(FE-40): create SuspenseHelper component 2025-10-09 11:05:30 +07:00
ValdiANS 6353b3aee4 feat(FE-40,41): create Master Data Edit FCR page 2025-10-09 10:05:24 +07:00
ValdiANS 527a155997 feat(FE-40,41): create Master Data Detail FCR page 2025-10-09 10:05:17 +07:00
ValdiANS f6163b1f69 feat(FE-40,41): create Master Data Add FCR page 2025-10-09 10:05:07 +07:00
ValdiANS d771b20956 feat(FE-43): create Master Data FCR page 2025-10-09 10:04:55 +07:00
ValdiANS da91201dde chore(FE-40): use optional chaining 2025-10-09 10:04:42 +07:00
ValdiANS 9b13ce2be6 chore(FE-40): render error message if isError and errorMessage exist 2025-10-09 10:04:20 +07:00
ValdiANS 764dacc627 feat(FE-43): create FcrsTable component 2025-10-09 10:04:01 +07:00
rstubryan d1f43c4e42 refactor: rename layout files for consistency and clarity 2025-10-09 10:00:38 +07:00
ValdiANS 5c0da471ae feat(FE-42): create Fcr form validation schema 2025-10-09 10:00:29 +07:00
ValdiANS 95556bfdd7 feat(FE-40,41): create FcrForm component 2025-10-09 09:59:53 +07:00
ValdiANS 24269d8c76 feat(FE-41): create Fcr api service 2025-10-09 09:59:34 +07:00
ValdiANS a6be56e6f2 feat(FE-41): create Fcr api type 2025-10-09 09:57:41 +07:00
ValdiANS f8f5e8403a chore(FE-40): create main drawer menu type 2025-10-09 09:55:14 +07:00
rstubryan 9f2add3a57 feat: add multiple layout components with suspense fallback for loading states 2025-10-09 09:38:16 +07:00
rstubryan 3051e931ca fix: enhance MainDrawer with typed MenuItem structure and submenu handling 2025-10-09 09:12:07 +07:00
randy-ar 21cc01fe68 feat(FE-33): create customers table and details 2025-10-09 04:36:57 +07:00
sweetpotet 21b9396323 feat(FE-33): create customers forms 2025-10-08 16:40:30 +07:00
ValdiANS 1d7f100507 feat(FE-40,41): create Master Data Edit Bank page 2025-10-08 15:38:10 +07:00
ValdiANS 372f1698ca feat(FE-40,41): create Master Data Detail Bank page 2025-10-08 15:38:01 +07:00
ValdiANS 0d5e8383fd feat(FE-40,41): create Master Data Add Bank page 2025-10-08 15:37:26 +07:00
ValdiANS b9015ed673 feat(FE-43): create Master Data Bank page 2025-10-08 15:37:01 +07:00
ValdiANS ca42570a40 feat(FE-43): create BanksTable component 2025-10-08 15:36:13 +07:00
ValdiANS 16a15fce66 feat(FE-42): create Bank form validation schema 2025-10-08 15:35:56 +07:00
ValdiANS 8c507aa410 feat(FE-40,41): create BankForm component 2025-10-08 15:35:34 +07:00
ValdiANS 10749f06da feat(FE-41): create Bank API service 2025-10-08 15:35:15 +07:00
ValdiANS 293f457ecb feat(FE-41): create Bank type 2025-10-08 15:35:00 +07:00
ValdiANS 780c0bb9d0 chore(FE-41): use Nonstock API service 2025-10-08 15:01:45 +07:00
ValdiANS b8548b72c9 feat(FE-40,41): create Nonstock form validation schema 2025-10-08 15:00:41 +07:00
ValdiANS c53f91ec3f feat(FE-43): create NonstocksTable component 2025-10-08 15:00:30 +07:00
ValdiANS 96fea80f62 feat(FE-40,41): create NonstockForm component 2025-10-08 15:00:06 +07:00
ValdiANS d24d50474d feat(FE-41): create Nonstock API service 2025-10-08 14:59:39 +07:00
ValdiANS f3d0e12bcd feat(FE-42): create flags type 2025-10-08 14:59:14 +07:00
ValdiANS 143d640a1e chore(FE-41): refactor nonstock type 2025-10-08 14:58:54 +07:00
ValdiANS 0e49e29002 feat(FE-42): create SUPPLIER_FLAG_OPTIONS constant 2025-10-08 14:58:21 +07:00
ValdiANS 8461667ca2 chore(FE-41): delete nonstock api helper function file 2025-10-08 14:56:52 +07:00
ValdiANS 3e7da624aa feat: add .prettierrc.json config 2025-10-08 13:47:56 +07:00
Rivaldi A N S 1968761b5d Merge branch 'feat/FE/US-33/TASK-40-slicing-ui-for-master-data-product-and-product-category-forms' into 'feat/FE/US-33/TASK-40-slicing-ui-for-master-data-forms'
[FEAT/FE][US#33/TASK#40-43] Implement Product & Product Category Feature (Type, Validation, Form, Table, and Pages)

See merge request mbugroup/lti-web-client!3
2025-10-08 03:08:01 +00:00
rstubryan 396ebe5001 feat(FE-43): add 'Code' column to Product Category table 2025-10-08 09:55:19 +07:00
rstubryan f5952f5a36 feat(FE-43): add Supplier API and data types for supplier management 2025-10-08 09:12:49 +07:00
rstubryan 250c42a04b feat(FE-40,41,42): add Product management pages with form handling and table display 2025-10-07 21:23:12 +07:00
rstubryan e1569c607c feat(FE-41,43): add Products table with CRUD operations and search functionality 2025-10-07 21:22:05 +07:00
rstubryan 3241cc9868 feat(FE-42): add Product API and form validation schema with product flags 2025-10-07 21:21:24 +07:00
rstubryan 26ec456937 feat(FE-40,41,42): add Product Category detail and edit pages with form handling 2025-10-07 18:59:31 +07:00
rstubryan 2b9b8e9920 feat(FE-41,43): implement Product Category table with CRUD operations 2025-10-07 18:58:38 +07:00
rstubryan 3cac49725f feat(FE-42): add Product Category API and form validation schema 2025-10-07 18:56:58 +07:00
rstubryan 4f0e02a93b chore: add .idea to .gitignore 2025-10-07 17:23:58 +07:00
ValdiANS af60e682ee chore(FE-41): redirect to /404 if response error 2025-10-05 16:19:37 +07:00
ValdiANS dcebd53c45 feat(FE-40): add edit button 2025-10-05 16:13:04 +07:00
ValdiANS 1ae2d13335 feat(FE-40,41): create Master Data Edit Warehouse page 2025-10-05 16:07:27 +07:00
ValdiANS 452139eeed feat(FE-40,41): create Master Data Detail Warehouse page 2025-10-05 16:07:17 +07:00
ValdiANS eb10dfe29f feat(FE-40,41): create Master Data Add Warehouse page 2025-10-05 16:07:06 +07:00
ValdiANS 70bdfc3b43 feat(FE-43): create Master Data Warehouse page 2025-10-05 16:06:51 +07:00
ValdiANS c1bc7beb4a feat(FE-43): create WarehousesTable component 2025-10-05 16:06:36 +07:00
ValdiANS 76cd64de5b feat(FE-42): create Warehouse form validation schema 2025-10-05 16:06:21 +07:00
ValdiANS 07691bfd9e feat(FE-40,41): create WarehouseForm component 2025-10-05 16:05:58 +07:00
ValdiANS 1f0c58d264 feat(FE-40): create WAREHOUSE_TYPE_OPTIONS constant 2025-10-05 16:05:33 +07:00
ValdiANS 19ce3989ba feat(FE-41): create Warehouse API service 2025-10-05 16:05:18 +07:00
ValdiANS a136ee1190 chore(FE-41): create Override type 2025-10-05 16:04:59 +07:00
ValdiANS acd28e5deb feat(FE-41): create warehouse api type 2025-10-05 16:04:46 +07:00
ValdiANS bfc81da349 feat(FE-40,41): create Master Data Edit Kandang page 2025-10-05 13:42:22 +07:00
ValdiANS 64bb87f92f feat(FE-40,41): create Master Data Detail Kandang page 2025-10-05 13:42:14 +07:00
ValdiANS f0c2910469 feat(FE-40,41): create Master Data Add Kandang page 2025-10-05 13:42:05 +07:00
ValdiANS 952110d7af feat(FE-43): create Master Data Kandang page 2025-10-05 13:41:45 +07:00
ValdiANS 6441a38a9d feat(FE-42): create Kandang form validation schema 2025-10-05 13:41:27 +07:00
ValdiANS 531a257e78 feat(FE-40,41): create KandangForm component 2025-10-05 13:41:08 +07:00
ValdiANS be844312d3 feat(FE-43): create KandangsTable component 2025-10-05 13:40:46 +07:00
ValdiANS 6ff19f05fd feat(FE-41): craete KandangApi service 2025-10-05 13:40:09 +07:00
ValdiANS 26093034fa feat(FE-41): create user API service 2025-10-05 13:39:38 +07:00
ValdiANS 0e5b718fd7 feat(FE-41): create kandang type 2025-10-05 13:39:25 +07:00
ValdiANS 0675d95a2a feat(FE-41): create user type 2025-10-05 13:39:16 +07:00
ValdiANS d5294e9b0b chore(FE-43): remove unnecessary code 2025-10-05 13:01:30 +07:00
ValdiANS 8c84e08f3b chore(FE-41): use .d.ts extension for types 2025-10-05 13:00:58 +07:00
ValdiANS f32e1ceec4 chore(FE-41): use BaseApiService class 2025-10-05 12:59:35 +07:00
ValdiANS f7b0933c0f feat(FE-41): create BaseApiService class 2025-10-05 12:59:15 +07:00
ValdiANS 508a530c3a chore(FE-43): add address and area sorting 2025-10-05 12:32:51 +07:00
ValdiANS 05a67bdc75 feat(FE-40,41): create Master Data Edit Location page 2025-10-04 14:59:44 +07:00
ValdiANS 54d2c85677 feat(FE-40,41): create Master Data Detail Location page 2025-10-04 14:59:30 +07:00
ValdiANS 288e4b92ff feat(FE-40,41): create Master Data Add Location page 2025-10-04 14:59:16 +07:00
ValdiANS 7e0dd1bdb1 feat(FE-43): create Master Data Location page 2025-10-04 14:59:03 +07:00
ValdiANS 57e5fafabd feat(FE-42): create Location form validation schema 2025-10-04 14:58:45 +07:00
ValdiANS e53d4e22b2 feat(FE-40,41): create LocationForm component 2025-10-04 14:58:25 +07:00
ValdiANS 3c0babb62b feat(FE-43): create LocationsTable component 2025-10-04 14:57:59 +07:00
ValdiANS e7e5456d15 feat(FE-41): create LocationApi service 2025-10-04 14:57:43 +07:00
ValdiANS d3977a0951 feat(FE-41): create Location type 2025-10-04 14:57:17 +07:00
ValdiANS 5b1dab2860 feat(FE-40): add onInputChange prop 2025-10-04 14:53:05 +07:00
ValdiANS 8ed12578b4 chore(FE-40): fix name input placeholder 2025-10-04 14:32:42 +07:00
ValdiANS 7ea599168c chore(FE-43): add conditional to set sorting and setSorting and add manualSorting props 2025-10-04 14:25:29 +07:00
ValdiANS a2345165c1 chore(FE-41): create BaseArea type 2025-10-04 14:09:03 +07:00
ValdiANS e1c34cf0fb chore(FE-41): create BaseUom type 2025-10-04 14:08:53 +07:00
ValdiANS 4332881ba6 chore(FE-40): add delete button and delete confirmation modal 2025-10-04 14:03:53 +07:00
ValdiANS 73cefbb7a3 chore(FE-40): change delete confirmation modal text 2025-10-04 14:03:27 +07:00
ValdiANS 9ba7b5dba4 feat(FE-40,41): create Master Data Edit Area page 2025-10-04 13:58:53 +07:00
ValdiANS 172d8efd8e feat(FE-40,41): create Master Data Detail Area page 2025-10-04 13:58:11 +07:00
ValdiANS 69ecacc1be feat(FE-40,41): create Master Data Add Area page 2025-10-04 13:57:48 +07:00
ValdiANS 4bf4981fd4 feat(FE-43): create Master Data Area page 2025-10-04 13:57:21 +07:00
ValdiANS 6dd6147c29 feat(FE-41): create AreaApi service 2025-10-04 13:57:05 +07:00
ValdiANS c494f8dbd5 feat(FE-41): create Area type 2025-10-04 13:56:51 +07:00
ValdiANS 211951132a feat(FE-42): create Area form validation schema 2025-10-04 13:56:17 +07:00
ValdiANS b82637fb3b feat(FE-40,41): create AreaForm component 2025-10-04 13:55:52 +07:00
ValdiANS eebc9940cc feat(FE-43): create AreasTable component 2025-10-04 13:55:28 +07:00
ValdiANS 777f0f5e81 feat(FE-40): add none type to Color 2025-10-04 12:23:31 +07:00
ValdiANS d29d1f27f8 chore(FE-41): create CreatedUser and BaseMetadata type 2025-10-04 12:23:14 +07:00
ValdiANS bbe55ee4c3 chore(FE-43): add ROWS_OPTIONS constant 2025-10-04 12:22:43 +07:00
ValdiANS 6dec9268c9 feat(FE-41): create Master Data API Service class 2025-10-04 12:22:25 +07:00
ValdiANS b5d9c55fbc chore(FE-41): create UOM type 2025-10-04 12:22:00 +07:00
ValdiANS d941674f9a feat(FE-40,41): create Master Data Edit UOM page 2025-10-04 12:21:38 +07:00
ValdiANS e6c14f57d9 feat(FE-40,41): create Master Data Detail UOM page 2025-10-04 12:21:14 +07:00
ValdiANS f27b261869 feat(FE-40,41): create Master Data Add UOM page 2025-10-04 12:20:19 +07:00
ValdiANS 6a396ccce6 feat(FE-43): create Master Data UOM page 2025-10-04 12:19:52 +07:00
ValdiANS 65e3833cd5 feat(FE-43): create RowDropdownOptions component 2025-10-04 12:19:15 +07:00
ValdiANS 34e9e60173 feat(FE-43): create RowCollapseOptions component 2025-10-04 12:19:01 +07:00
ValdiANS f1a8fda667 feat(FE-42): create UOM form validation schema 2025-10-04 12:18:39 +07:00
ValdiANS 36113f6c2a feat(FE-40,41): create UomForm component 2025-10-04 12:18:15 +07:00
ValdiANS e259d1720c feat(FE-43): create UomsTable component 2025-10-04 12:17:42 +07:00
ValdiANS 78750060de feat(FE-40): create ConfirmationModal component 2025-10-04 12:17:17 +07:00
ValdiANS ae159b9617 chore(FE-40): remove unnecessary dependencies 2025-10-04 12:15:06 +07:00
ValdiANS 56476c7dd9 chore(FE-40): adjust loading dots size 2025-10-04 12:14:47 +07:00
ValdiANS fa5d09e4fb chore(FE-40): add Toaster component in root layout 2025-10-04 12:14:18 +07:00
ValdiANS df1b4c29e5 feat(FE-43): create useTableFilter hooks 2025-10-04 12:13:49 +07:00
ValdiANS 20f6686afc chore(FE-43): add sorting and setSorting props 2025-10-04 12:08:08 +07:00
ValdiANS 18027f0bb9 chore(FE-43): get setPageSize from table object 2025-10-04 11:31:23 +07:00
ValdiANS 2976ffffbf chore(FE-40): install use-debounce 2025-10-04 11:18:23 +07:00
ValdiANS 5983a44311 chore(FE-40): add DebouncedTextInput component 2025-10-04 11:18:08 +07:00
ValdiANS 42dd91117e chore(FE-40): install react-hot-toast 2025-10-04 11:01:21 +07:00
ValdiANS 60d0d77dff feat(FE-40): add Alert component 2025-10-03 22:13:35 +07:00
Adnan Zahir 7723e2a8d3 Merge branch 'chore/CI/merge-request-notify-workflow' into 'development'
chore(CI): added gitlab ci yaml file for notify MR and MR-merged events

See merge request mbugroup/lti-web-client!2
2025-10-03 22:01:25 +07:00
Adnan Zahir 88fe135cb4 chore(CI): added gitlab ci yaml file for notify MR and MR-merged events 2025-10-03 21:58:55 +07:00
ValdiANS 83701a9689 chore(FE-40): update table pageSize if it change 2025-10-03 14:12:02 +07:00
ValdiANS e765a7a5fb chore(FE-40): hide empty content if is loading 2025-10-02 16:50:37 +07:00
ValdiANS 75a5caa63b feat(FE-40): create Nonstock Edit page 2025-10-02 12:03:23 +07:00
ValdiANS c40c707c17 feat(FE-40): create Nonstock Detail page 2025-10-02 12:03:06 +07:00
ValdiANS c3da39ef1b feat(FE-40): create Add Nonstock page 2025-10-02 12:02:21 +07:00
ValdiANS 230e966197 feat(FE-40): create Nonstock page 2025-10-02 12:02:11 +07:00
ValdiANS 62b3894983 feat(FE-40): create api type for nonstock 2025-10-02 12:02:02 +07:00
ValdiANS 8dd1ebdfe4 feat(FE-40): create api service for nonstock 2025-10-02 12:01:43 +07:00
ValdiANS 35c809193b feat(FE-40): create NonstocksTable component 2025-10-02 12:01:14 +07:00
ValdiANS e6acfc1214 feat(FE-40): create NonstockForm component 2025-10-02 12:01:00 +07:00
ValdiANS 36b66d9b2f feat(FE-40): create Dashboard page 2025-10-02 12:00:18 +07:00
ValdiANS 5c73f8f4af chore: update toggle dependencies 2025-10-02 12:00:02 +07:00
ValdiANS 2a6f2a1646 chore(FE-40): update MainDrawer component styling 2025-10-02 11:46:27 +07:00
ValdiANS d40a5dd898 chore: update Collapse styling 2025-10-02 11:46:09 +07:00
ValdiANS ca9205618a feat: add Modal component 2025-10-02 11:45:11 +07:00
ValdiANS 14046a1add chore(FE-40): create isResponseError api helper function 2025-10-01 16:02:12 +07:00
ValdiANS 8ad49a4480 chore(FE-40): export ErrorApiResponse and SucessApiResponse type 2025-10-01 16:01:47 +07:00
ValdiANS 4ff196cb9d chore(FE-40): remove unnecessary gap 2025-10-01 15:32:35 +07:00
ValdiANS 0afde48135 chore(FE-40): set correct page title 2025-10-01 15:24:33 +07:00
ValdiANS 9b2930375d chore(FE-40): fix bank link 2025-10-01 15:00:44 +07:00
ValdiANS 8a6a1e6b5c chore(FE-40): use RequireAuth in root layout 2025-10-01 15:00:06 +07:00
ValdiANS 6924aef8c4 feat(FE-40): create RequireAuth helper component 2025-10-01 14:59:46 +07:00
ValdiANS fa96d7a98a chore(FE-40): set opts.auth default to 'cookie' and export SWRHttpKey type 2025-10-01 14:08:02 +07:00
ValdiANS 3d3df42576 chore(FE-40): update import path and return isLoadingUser and setIsLoadingUser in useAuth 2025-10-01 14:07:30 +07:00
ValdiANS 05886896f1 chore(FE-40): update import path 2025-10-01 14:07:04 +07:00
ValdiANS a347024188 chore(FE-40): update CollapseMenu styling 2025-10-01 14:04:50 +07:00
ValdiANS 6969a2bcb8 chore(FE-40): move api.d.ts to /types/api/api-general.d.ts 2025-10-01 13:56:59 +07:00
ValdiANS 8206f7de5f feat(FE-40): create MainUiSlice type 2025-10-01 13:45:25 +07:00
ValdiANS a5b392ae76 feat(FE-40): create main UI slice 2025-10-01 13:45:16 +07:00
ValdiANS 6cbdff5398 feat(FE-40): add main UI slice to useUiStore 2025-10-01 13:45:00 +07:00
ValdiANS 87ce1e50aa feat(FE-40): create MAIN_DRAWER_LINKS constant 2025-10-01 13:44:42 +07:00
ValdiANS 2456d64a68 feat(FE-40): create MainDrawer component 2025-10-01 13:44:26 +07:00
ValdiANS c068fe5166 feat(FE-40): redirect to /dashboard 2025-10-01 13:44:18 +07:00
ValdiANS 19b7c53ec2 feat(FE-40): use MainDrawer component in root layout 2025-10-01 13:44:02 +07:00
ValdiANS 037e4776a8 chore: run eslint command in dev command 2025-09-30 15:45:33 +07:00
ValdiANS 3be69eeff8 fix: error type 2025-09-30 15:44:48 +07:00
ValdiANS a364a860fa chore: update PasswordInput component 2025-09-30 15:43:48 +07:00
ValdiANS 8e23a805a9 chore: update Button component 2025-09-30 15:43:04 +07:00
ValdiANS e052166b3a chore: update Pagination component 2025-09-30 15:42:25 +07:00
ValdiANS 691b49a902 chore: update Table component 2025-09-30 15:42:03 +07:00
ValdiANS 33f5ca2a57 chore: update Table component 2025-09-30 15:33:57 +07:00
ValdiANS 02c44ced92 chore: update Collapse component 2025-09-30 11:25:38 +07:00
ValdiANS 978285021e feat: add Collapse component 2025-09-29 11:58:57 +07:00
ValdiANS 7e57debb98 chore: update MenuItem component 2025-09-29 11:58:19 +07:00
Adnan Zahir a5d8ac47fe Merge branch 'init' into 'development'
init

See merge request mbugroup/lti-web-client!1
2025-09-26 11:41:47 +07:00
ValdiANS f127e16c7c chore: change copywriting 2025-09-26 11:36:53 +07:00
ValdiANS 2e1b0fef2b init 2025-09-26 11:06:31 +07:00
173 changed files with 22187 additions and 79 deletions
+47
View File
@@ -0,0 +1,47 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# prettier
.prettierrc
# idea
.idea
+76
View File
@@ -0,0 +1,76 @@
stages: [notify]
# --- Notify when MR is opened/updated ---
notify_discord_mr:
stage: notify
image: alpine:3.20
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
variables:
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
before_script:
- apk add --no-cache curl jq
script: |
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
jq -n \
--arg repo "$CI_PROJECT_PATH" \
--arg mr "#${CI_MERGE_REQUEST_IID}" \
--arg url "$MR_URL" \
--arg requestor "${GITLAB_USER_LOGIN:-$GITLAB_USER_NAME}" \
--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 Opened/Updated",
description: ($mr + " in " + $repo),
url: $url,
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 ---
notify_discord_merge:
stage: notify
image: alpine:3.20
rules:
# Only run for merge request pipelines that are in merged state
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"'
variables:
WEBHOOK_URL: $DISCORD_WEBHOOK_URL
before_script:
- apk add --no-cache curl jq
script: |
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
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"
+15
View File
@@ -0,0 +1,15 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"endOfLine": "lf",
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"semi": true,
"tabWidth": 2,
"trailingComma": "es5"
}
+15
View File
@@ -0,0 +1,15 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"endOfLine": "lf",
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"semi": true,
"tabWidth": 2,
"trailingComma": "es5"
}
+22 -79
View File
@@ -1,93 +1,36 @@
# LTI - Client
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://gitlab.com/mbugroup/lti-client.git
git branch -M main
git push -uf origin main
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
## Integrate with your tools
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
- [ ] [Set up project integrations](https://gitlab.com/mbugroup/lti-client/-/settings/integrations)
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## Collaborate with your team
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
## Learn More
## Test and Deploy
To learn more about Next.js, take a look at the following resources:
Use the built-in continuous integration in GitLab.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
***
## Deploy on Vercel
# Editing this README
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+25
View File
@@ -0,0 +1,25 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
];
export default eslintConfig;
+8
View File
@@ -0,0 +1,8 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'export',
images: { unoptimized: true },
};
export default nextConfig;
+7047
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "lti-web-client",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "eslint && next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.12.2",
"clsx": "^2.1.1",
"formik": "^2.4.6",
"moment": "^2.30.1",
"next": "15.5.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hot-toast": "^2.6.0",
"react-select": "^5.10.2",
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"use-debounce": "^10.0.6",
"yup": "^1.7.0",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"daisyui": "^5.1.12",
"eslint": "^9",
"eslint-config-next": "15.5.3",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+5
View File
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

+9
View File
@@ -0,0 +1,9 @@
const Dashboard = () => {
return (
<section className='w-full p-4'>
<h1 className='text-3xl font-bold text-primary'>Dashboard</h1>
</section>
);
};
export default Dashboard;
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+14
View File
@@ -0,0 +1,14 @@
@import 'tailwindcss';
@plugin "daisyui";
:root {
--color-primary: #1f74bf;
}
@theme {
--font-inter: var(--font-inter);
}
html {
scrollbar-gutter: initial;
}
+11
View File
@@ -0,0 +1,11 @@
import InventoryAdjustmentForm from "@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm";
const CreateInventoryAdjustment = () => {
return (
<section className="w-full p-4 flex flex-row justify-center">
<InventoryAdjustmentForm/>
</section>
);
}
export default CreateInventoryAdjustment;
@@ -0,0 +1,46 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import InventoryAdjustmentForm from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm';
import type { InventoryAdjustment } from '@/types/api/inventory/adjustment';
const DetailInventoryAdjustment = () => {
const router = useRouter();
const [inventoryAdjustment, setInventoryAdjustment] = useState<InventoryAdjustment | null>(null);
// Ambil data dari router state
useEffect(() => {
console.log("Router State");
console.log(window.history.state);
const state = window.history.state?.usr as
| { inventoryAdjustment?: InventoryAdjustment }
| undefined;
if (state?.inventoryAdjustment) {
// jika object dikirim via router.push(state)
setInventoryAdjustment(state.inventoryAdjustment);
}
}, [router]);
const finalData = inventoryAdjustment;
console.log("Final Data");
console.log(finalData);
if (!finalData) {
return (
<div className="w-full flex flex-row justify-center items-center p-4">
<span className="loading loading-spinner loading-xl" />
</div>
);
}
return (
<section className="w-full p-4 flex flex-row justify-center">
<InventoryAdjustmentForm initialValues={finalData} />
</section>
);
};
export default DetailInventoryAdjustment;
+11
View File
@@ -0,0 +1,11 @@
import InventoryAdjustmentTable from '@/components/pages/inventory/adjustment/InventoryAdjustmentTable';
const InventoryAdjustment = () => {
return (
<section className='w-full p-4'>
<InventoryAdjustmentTable />
</section>
);
};
export default InventoryAdjustment;
+41
View File
@@ -0,0 +1,41 @@
import type { Metadata, Viewport } from 'next';
import { Inter } from 'next/font/google';
import '@/app/globals.css';
import { Toaster } from 'react-hot-toast';
import MainDrawer from '@/components/MainDrawer';
import RequireAuth from '@/components/helper/RequireAuth';
const inter = Inter({
variable: '--font-inter',
subsets: ['latin'],
});
export const viewport: Viewport = {
themeColor: '#1f74bf',
colorScheme: 'light',
initialScale: 1,
};
export const metadata: Metadata = {
title: 'LTI',
description: 'PT. Lumbung Telur Indonesia',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang='en'>
<body className={`${inter.variable} antialiased font-inter`}>
<RequireAuth>
<MainDrawer>{children}</MainDrawer>
</RequireAuth>
<Toaster />
</body>
</html>
);
}
+11
View File
@@ -0,0 +1,11 @@
import AreaForm from '@/components/pages/master-data/area/form/AreaForm';
const AddNonstock = () => {
return (
<div className='w-full p-4 flex flex-row justify-center'>
<AreaForm />
</div>
);
};
export default AddNonstock;
@@ -0,0 +1,47 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import AreaForm from '@/components/pages/master-data/area/form/AreaForm';
import { AreaApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const AreaEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const areaId = searchParams.get('areaId');
const { data: area, isLoading: isLoadingArea } = useSWR(
areaId,
(id: number) => AreaApi.getSingle(id)
);
if (!areaId) {
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 (!isLoadingArea && (!area || isResponseError(area))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingArea && <span className='loading loading-spinner loading-xl' />}
{!isLoadingArea && isResponseSuccess(area) && (
<AreaForm type='edit' initialValues={area.data} />
)}
</div>
);
};
export default AreaEdit;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
+47
View File
@@ -0,0 +1,47 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import AreaForm from '@/components/pages/master-data/area/form/AreaForm';
import { AreaApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const AreaDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const areaId = searchParams.get('areaId');
const { data: area, isLoading: isLoadingArea } = useSWR(
areaId,
(id: number) => AreaApi.getSingle(id)
);
if (!areaId) {
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 (!isLoadingArea && (!area || isResponseError(area))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingArea && <span className='loading loading-spinner loading-xl' />}
{!isLoadingArea && isResponseSuccess(area) && (
<AreaForm type='detail' initialValues={area.data} />
)}
</div>
);
};
export default AreaDetail;
+11
View File
@@ -0,0 +1,11 @@
import AreasTable from '@/components/pages/master-data/area/AreasTable';
const Nonstock = () => {
return (
<section className='w-full p-4'>
<AreasTable />
</section>
);
};
export default Nonstock;
+11
View File
@@ -0,0 +1,11 @@
import BankForm from '@/components/pages/master-data/bank/form/BankForm';
const AddBank = () => {
return (
<div className='w-full p-4 flex flex-row justify-center'>
<BankForm />
</div>
);
};
export default AddBank;
@@ -0,0 +1,47 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import BankForm from '@/components/pages/master-data/bank/form/BankForm';
import { BankApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const BankEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const bankId = searchParams.get('bankId');
const { data: bank, isLoading: isLoadingBank } = useSWR(
bankId,
(id: number) => BankApi.getSingle(id)
);
if (!bankId) {
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 (!isLoadingBank && (!bank || isResponseError(bank))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingBank && <span className='loading loading-spinner loading-xl' />}
{!isLoadingBank && isResponseSuccess(bank) && (
<BankForm type='edit' initialValues={bank.data} />
)}
</div>
);
};
export default BankEdit;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
+47
View File
@@ -0,0 +1,47 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import BankForm from '@/components/pages/master-data/bank/form/BankForm';
import { BankApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const BankDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const bankId = searchParams.get('bankId');
const { data: bank, isLoading: isLoadingBank } = useSWR(
bankId,
(id: number) => BankApi.getSingle(id)
);
if (!bankId) {
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 (!isLoadingBank && (!bank || isResponseError(bank))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingBank && <span className='loading loading-spinner loading-xl' />}
{!isLoadingBank && isResponseSuccess(bank) && (
<BankForm type='detail' initialValues={bank.data} />
)}
</div>
);
};
export default BankDetail;
+11
View File
@@ -0,0 +1,11 @@
import BanksTable from '@/components/pages/master-data/bank/BanksTable';
const Bank = () => {
return (
<section className='w-full p-4'>
<BanksTable />
</section>
);
};
export default Bank;
+11
View File
@@ -0,0 +1,11 @@
import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm";
const AddCustomer = () => {
return (
<section className="w-full p-4 flex flex-row justify-center">
<CustomerForm/>
</section>
);
}
export default AddCustomer;
@@ -0,0 +1,47 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { CustomerApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import CustomerForm from '@/components/pages/master-data/customer/form/CustomerForm';
const CustomerEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const costumerId = searchParams.get('customerId');
const { data: costumer, isLoading: isLoadingCostumer } = useSWR(
costumerId,
(id: number) => CustomerApi.getSingle(id)
);
if (!costumerId) {
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 (!isLoadingCostumer && (!costumer || isResponseError(costumer))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingCostumer && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingCostumer && isResponseSuccess(costumer) && (
<CustomerForm formType='edit' initialValues={costumer.data} />
)}
</div>
);
};
export default CustomerEdit;
@@ -0,0 +1,45 @@
'use client'
import { useRouter, useSearchParams } from "next/navigation";
import useSWR from "swr";
import { CustomerApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm";
const CustomerDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const costumerId = searchParams.get("customerId");
const { data: costumer, isLoading: isLoadingCostumer } = useSWR(
costumerId,
(id: number) => CustomerApi.getSingle(id)
);
if(!costumerId){
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(!isLoadingCostumer && (!costumer || isResponseError(costumer))){
router.replace("/404");
return;
}
return (
<div className="w-full p-4 flex flex-row justify-center">
{isLoadingCostumer && <span className="loading loading-spinner loading-xl" />}
{!isLoadingCostumer && isResponseSuccess(costumer) && (
<CustomerForm formType="detail" initialValues={costumer.data} />
)}
</div>
)
};
export default CustomerDetail;
+11
View File
@@ -0,0 +1,11 @@
import CustomersTable from "@/components/pages/master-data/customer/CustomersTable";
const Customer = () => {
return (
<section className="w-full p-4">
<CustomersTable />
</section>
)
};
export default Customer;
+11
View File
@@ -0,0 +1,11 @@
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
const AddFcr = () => {
return (
<div className='w-full p-4 flex flex-row justify-center'>
<FcrForm />
</div>
);
};
export default AddFcr;
@@ -0,0 +1,52 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
import { FcrApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse } from '@/types/api/api-general';
import { FcrWithStandards } from '@/types/api/master-data/fcr';
const FcrEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const fcrId = searchParams.get('fcrId');
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
fcrId,
(id: number) =>
FcrApi.getSingle(id) as Promise<
BaseApiResponse<FcrWithStandards> | undefined
>
);
if (!fcrId) {
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 (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
{!isLoadingFcr && isResponseSuccess(fcr) && (
<FcrForm type='edit' initialValues={fcr.data} />
)}
</div>
);
};
export default FcrEdit;
+11
View File
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
+52
View File
@@ -0,0 +1,52 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
import { FcrApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { FcrWithStandards } from '@/types/api/master-data/fcr';
import { BaseApiResponse } from '@/types/api/api-general';
const FcrDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const fcrId = searchParams.get('fcrId');
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
fcrId,
(id: number) =>
FcrApi.getSingle(id) as Promise<
BaseApiResponse<FcrWithStandards> | undefined
>
);
if (!fcrId) {
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 (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
{!isLoadingFcr && isResponseSuccess(fcr) && (
<FcrForm type='detail' initialValues={fcr.data} />
)}
</div>
);
};
export default FcrDetail;
+11
View File
@@ -0,0 +1,11 @@
import FcrsTable from '@/components/pages/master-data/fcr/FcrsTable';
const Fcr = () => {
return (
<section className='w-full p-4'>
<FcrsTable />
</section>
);
};
export default Fcr;
+11
View File
@@ -0,0 +1,11 @@
import KandangForm from '@/components/pages/master-data/kandang/form/KandangForm';
const AddNonstock = () => {
return (
<div className='w-full p-4 flex flex-row justify-center'>
<KandangForm />
</div>
);
};
export default AddNonstock;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import KandangForm from '@/components/pages/master-data/kandang/form/KandangForm';
import { KandangApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const KandangEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: kandang, isLoading: isLoadingKandang } = useSWR(
kandangId,
(id: number) => KandangApi.getSingle(id)
);
if (!kandangId) {
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 (!isLoadingKandang && (!kandang || isResponseError(kandang))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingKandang && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingKandang && isResponseSuccess(kandang) && (
<KandangForm type='edit' initialValues={kandang.data} />
)}
</div>
);
};
export default KandangEdit;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import KandangForm from '@/components/pages/master-data/kandang/form/KandangForm';
import { KandangApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const KandangDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: kandang, isLoading: isLoadingKandang } = useSWR(
kandangId,
(id: number) => KandangApi.getSingle(id)
);
if (!kandangId) {
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 (!isLoadingKandang && (!kandang || isResponseError(kandang))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingKandang && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingKandang && isResponseSuccess(kandang) && (
<KandangForm type='detail' initialValues={kandang.data} />
)}
</div>
);
};
export default KandangDetail;
+11
View File
@@ -0,0 +1,11 @@
import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable';
const Nonstock = () => {
return (
<section className='w-full p-4'>
<KandangsTable />
</section>
);
};
export default Nonstock;
+11
View File
@@ -0,0 +1,11 @@
import LocationForm from '@/components/pages/master-data/location/form/LocationForm';
const AddNonstock = () => {
return (
<div className='w-full p-4 flex flex-row justify-center'>
<LocationForm />
</div>
);
};
export default AddNonstock;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import LocationForm from '@/components/pages/master-data/location/form/LocationForm';
import { LocationApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const LocationEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const locationId = searchParams.get('locationId');
const { data: location, isLoading: isLoadingLocation } = useSWR(
locationId,
(id: number) => LocationApi.getSingle(id)
);
if (!locationId) {
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 (!isLoadingLocation && (!location || isResponseError(location))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingLocation && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingLocation && isResponseSuccess(location) && (
<LocationForm type='edit' initialValues={location.data} />
)}
</div>
);
};
export default LocationEdit;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import LocationForm from '@/components/pages/master-data/location/form/LocationForm';
import { LocationApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const LocationDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const locationId = searchParams.get('locationId');
const { data: location, isLoading: isLoadingLocation } = useSWR(
locationId,
(id: number) => LocationApi.getSingle(id)
);
if (!locationId) {
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 (!isLoadingLocation && (!location || isResponseError(location))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingLocation && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingLocation && isResponseSuccess(location) && (
<LocationForm type='detail' initialValues={location.data} />
)}
</div>
);
};
export default LocationDetail;
+11
View File
@@ -0,0 +1,11 @@
import LocationsTable from '@/components/pages/master-data/location/LocationsTable';
const Nonstock = () => {
return (
<section className='w-full p-4'>
<LocationsTable />
</section>
);
};
export default Nonstock;
+11
View File
@@ -0,0 +1,11 @@
import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm';
const AddNonstock = () => {
return (
<div className='w-full p-4 flex flex-row justify-center'>
<NonstockForm />
</div>
);
};
export default AddNonstock;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm';
import { NonstockApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const NonstockEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const nonstockId = searchParams.get('nonstockId');
const { data: nonstock, isLoading: isLoadingNonstock } = useSWR(
nonstockId,
(id: number) => NonstockApi.getSingle(id)
);
if (!nonstockId) {
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 (!isLoadingNonstock && (!nonstock || isResponseError(nonstock))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingNonstock && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingNonstock && isResponseSuccess(nonstock) && (
<NonstockForm type='edit' initialValues={nonstock.data} />
)}
</div>
);
};
export default NonstockEdit;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm';
import { NonstockApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const NonstockDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const nonstockId = searchParams.get('nonstockId');
const { data: nonstock, isLoading: isLoadingNonstock } = useSWR(
nonstockId,
(id: number) => NonstockApi.getSingle(id)
);
if (!nonstockId) {
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 (!isLoadingNonstock && (!nonstock || isResponseError(nonstock))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingNonstock && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingNonstock && isResponseSuccess(nonstock) && (
<NonstockForm type='detail' initialValues={nonstock.data} />
)}
</div>
);
};
export default NonstockDetail;
+11
View File
@@ -0,0 +1,11 @@
import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable';
const Nonstock = () => {
return (
<section className='w-full p-4'>
<NonstocksTable />
</section>
);
};
export default Nonstock;
@@ -0,0 +1,11 @@
import ProductCategoryForm from "@/components/pages/master-data/product-category/form/ProductCategoryForm";
const AddProductCategory = () => {
return (
<div className="w-full p-4 flex flex-row justify-center">
<ProductCategoryForm />
</div>
);
};
export default AddProductCategory;
@@ -0,0 +1,47 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import ProductCategoryForm from '@/components/pages/master-data/product-category/form/ProductCategoryForm';
import { ProductCategoryApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const ProductCategoryEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const productCategoryId = searchParams.get('productCategoryId');
const { data: productCategory, isLoading: isLoadingProductCategory } = useSWR(
productCategoryId,
(id: number) => ProductCategoryApi.getSingle(id)
);
if (!productCategoryId) {
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 (
<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;
@@ -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 ProductCategoryForm from '@/components/pages/master-data/product-category/form/ProductCategoryForm';
import { ProductCategoryApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const ProductCategoryDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const productCategoryId = searchParams.get('productCategoryId');
const { data: productCategory, isLoading: isLoadingProductCategory } = useSWR(
productCategoryId,
(id: number) => ProductCategoryApi.getSingle(id)
);
if (!productCategoryId) {
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 (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingProductCategory && <span className='loading loading-spinner loading-xl' />}
{!isLoadingProductCategory && isResponseSuccess(productCategory) && (
<ProductCategoryForm type='detail' initialValues={productCategory.data} />
)}
</div>
);
};
export default ProductCategoryDetail;
@@ -0,0 +1,11 @@
import ProductCategoryTable from "@/components/pages/master-data/product-category/ProductCategoryTable";
const ProductCategory = () => {
return (
<section className="w-full p-4">
<ProductCategoryTable />
</section>
);
};
export default ProductCategory;
+11
View File
@@ -0,0 +1,11 @@
import ProductForm from '@/components/pages/master-data/product/form/ProductForm';
const AddProduct = () => {
return (
<div className="w-full p-4 flex flex-row justify-center">
<ProductForm />
</div>
);
};
export default AddProduct;
@@ -0,0 +1,45 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import ProductForm from '@/components/pages/master-data/product/form/ProductForm';
import { ProductApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const ProductEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const productId = searchParams.get('productId');
const { data: product, isLoading } = useSWR(
productId,
(id: number) => ProductApi.getSingle(id)
);
if (!productId) {
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 && (!product || isResponseError(product))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(product) && (
<ProductForm type='edit' initialValues={product.data} />
)}
</div>
);
};
export default ProductEdit;
@@ -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,45 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import ProductForm from '@/components/pages/master-data/product/form/ProductForm';
import { ProductApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const ProductDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const productId = searchParams.get('productId');
const { data: product, isLoading } = useSWR(
productId,
(id: number) => ProductApi.getSingle(id)
);
if (!productId) {
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 && (!product || isResponseError(product))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(product) && (
<ProductForm type='detail' initialValues={product.data} />
)}
</div>
);
};
export default ProductDetail;
+11
View File
@@ -0,0 +1,11 @@
import ProductsTable from "@/components/pages/master-data/product/ProductTable";
const Product = () => {
return (
<section className="w-full p-4">
<ProductsTable />
</section>
);
};
export default Product;
+11
View File
@@ -0,0 +1,11 @@
import SupplierForm from '@/components/pages/master-data/supplier/form/SupplierForm';
const AddSupplier = () => {
return (
<section className='w-full p-4 flex flex-row justify-center'>
<SupplierForm />
</section>
);
};
export default AddSupplier;
@@ -0,0 +1,49 @@
'use client';
import SupplierForm from '@/components/pages/master-data/supplier/form/SupplierForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { SupplierApi } from '@/services/api/master-data';
import { useSearchParams, useRouter } from 'next/navigation';
import useSWR from 'swr';
const SupplierEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
// Get Query Params
const supplierId = searchParams.get('supplierId');
// Fetch Data
const { data: supplier, isLoading: isLoadingSupplier } = useSWR(
supplierId,
(id: number) => SupplierApi.getSingle(id)
);
if (!supplierId) {
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 (!isLoadingSupplier && (!supplier || isResponseError(supplier))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingSupplier && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingSupplier && isResponseSuccess(supplier) && (
<SupplierForm formType='edit' initialValues={supplier.data} />
)}
</div>
);
};
export default SupplierEdit;
@@ -0,0 +1,49 @@
'use client';
import SupplierForm from '@/components/pages/master-data/supplier/form/SupplierForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { SupplierApi } from '@/services/api/master-data';
import { useSearchParams, useRouter } from 'next/navigation';
import useSWR from 'swr';
const SupplierDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
// Get Query Params
const supplierId = searchParams.get('supplierId');
// Fetch Data
const { data: supplier, isLoading: isLoadingSupplier } = useSWR(
supplierId,
(id: number) => SupplierApi.getSingle(id)
);
if (!supplierId) {
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 (!isLoadingSupplier && (!supplier || isResponseError(supplier))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingSupplier && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingSupplier && isResponseSuccess(supplier) && (
<SupplierForm formType='detail' initialValues={supplier.data} />
)}
</div>
);
};
export default SupplierDetail;
+11
View File
@@ -0,0 +1,11 @@
import SuppliersTable from "@/components/pages/master-data/supplier/SupplierTable";
const Supplier = () => {
return (
<section className='w-full p-4'>
<SuppliersTable />
</section>
);
};
export default Supplier;
+11
View File
@@ -0,0 +1,11 @@
import UomForm from '@/components/pages/master-data/uom/form/UomForm';
const AddNonstock = () => {
return (
<div className='w-full p-4 flex flex-row justify-center'>
<UomForm />
</div>
);
};
export default AddNonstock;
@@ -0,0 +1,46 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import UomForm from '@/components/pages/master-data/uom/form/UomForm';
import { UomApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const UomEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const uomId = searchParams.get('uomId');
const { data: uom, isLoading: isLoadingUom } = useSWR(uomId, (id: number) =>
UomApi.getSingle(id)
);
if (!uomId) {
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 (!isLoadingUom && (!uom || isResponseError(uom))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingUom && <span className='loading loading-spinner loading-xl' />}
{!isLoadingUom && isResponseSuccess(uom) && (
<UomForm type='edit' initialValues={uom.data} />
)}
</div>
);
};
export default UomEdit;
+11
View File
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
+46
View File
@@ -0,0 +1,46 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import UomForm from '@/components/pages/master-data/uom/form/UomForm';
import { UomApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const UomDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const uomId = searchParams.get('uomId');
const { data: uom, isLoading: isLoadingUom } = useSWR(uomId, (id: number) =>
UomApi.getSingle(id)
);
if (!uomId) {
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 (!isLoadingUom && (!uom || isResponseError(uom))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingUom && <span className='loading loading-spinner loading-xl' />}
{!isLoadingUom && isResponseSuccess(uom) && (
<UomForm type='detail' initialValues={uom.data} />
)}
</div>
);
};
export default UomDetail;
+11
View File
@@ -0,0 +1,11 @@
import UomsTable from '@/components/pages/master-data/uom/UomsTable';
const Nonstock = () => {
return (
<section className='w-full p-4'>
<UomsTable />
</section>
);
};
export default Nonstock;
@@ -0,0 +1,11 @@
import WarehouseForm from '@/components/pages/master-data/warehouse/form/WarehouseForm';
const AddNonstock = () => {
return (
<div className='w-full p-4 flex flex-row justify-center'>
<WarehouseForm />
</div>
);
};
export default AddNonstock;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import WarehouseForm from '@/components/pages/master-data/warehouse/form/WarehouseForm';
import { WarehouseApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const WarehouseEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
const warehouseId = searchParams.get('warehouseId');
const { data: warehouse, isLoading: isLoadingWarehouse } = useSWR(
warehouseId,
(id: number) => WarehouseApi.getSingle(id)
);
if (!warehouseId) {
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 (!isLoadingWarehouse && (!warehouse || isResponseError(warehouse))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingWarehouse && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingWarehouse && isResponseSuccess(warehouse) && (
<WarehouseForm type='edit' initialValues={warehouse.data} />
)}
</div>
);
};
export default WarehouseEdit;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import WarehouseForm from '@/components/pages/master-data/warehouse/form/WarehouseForm';
import { WarehouseApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const WarehouseDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
const warehouseId = searchParams.get('warehouseId');
const { data: warehouse, isLoading: isLoadingWarehouse } = useSWR(
warehouseId,
(id: number) => WarehouseApi.getSingle(id)
);
if (!warehouseId) {
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 (!isLoadingWarehouse && (!warehouse || isResponseError(warehouse))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingWarehouse && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingWarehouse && isResponseSuccess(warehouse) && (
<WarehouseForm type='detail' initialValues={warehouse.data} />
)}
</div>
);
};
export default WarehouseDetail;
+11
View File
@@ -0,0 +1,11 @@
import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable';
const Warehouse = () => {
return (
<section className='w-full p-4'>
<WarehousesTable />
</section>
);
};
export default Warehouse;
+11
View File
@@ -0,0 +1,11 @@
import { redirect } from 'next/navigation';
export default function Home() {
redirect('/dashboard');
return (
<main className='w-full h-full min-h-screen flex flex-row justify-center items-center'>
<h1>LTI ERP</h1>
</main>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/helper';
interface AlertProps {
variant?: 'outline' | 'dash' | 'soft';
color?: 'info' | 'success' | 'warning' | 'error';
children?: ReactNode;
className?: string;
}
const Alert = ({ children, variant, color, className }: AlertProps) => {
const alertBaseClassName = cn('alert', {
'alert-soft': variant === 'soft',
'alert-outline': variant === 'outline',
'alert-dash': variant === 'dash',
'alert-info': color === 'info',
'alert-success': color === 'success',
'alert-warning': color === 'warning',
'alert-error': color === 'error',
});
return <div className={cn(alertBaseClassName, className)}>{children}</div>;
};
export default Alert;
+86
View File
@@ -0,0 +1,86 @@
import react from 'react';
import Link from 'next/link';
import { cn } from '@/lib/helper';
import { Color } from '@/types/theme';
interface ButtonProps extends react.ComponentProps<'button'> {
variant?: 'soft' | 'outline' | 'dash' | 'ghost' | 'link' | 'active';
color?: Color;
href?: string;
isLoading?: boolean;
}
const Button = ({
children,
type,
href,
variant,
color = 'primary',
isLoading,
className,
disabled,
onClick,
...props
}: ButtonProps) => {
const btnBaseClassName = cn(
'btn',
{
'btn-soft': variant === 'soft',
'btn-outline': variant === 'outline',
'btn-dash': variant === 'dash',
'btn-ghost': variant === 'ghost',
'btn-link': variant === 'link',
'btn-active': variant === 'active',
'btn-primary': color === 'primary',
'btn-secondary': color === 'secondary',
'btn-accent': color === 'accent',
'btn-neutral': color === 'neutral',
'btn-info': color === 'info',
'btn-success': color === 'success',
'btn-warning': color === 'warning',
'btn-error': color === 'error',
},
'h-fit justify-center items-center gap-2 rounded-xl p-2 text-base transition-all'
);
return (
<>
{!href && (
<button
{...props}
type={type}
onClick={onClick}
disabled={disabled}
className={cn(
btnBaseClassName,
'disabled:pointer-events-auto! disabled:cursor-not-allowed!',
className
)}
>
{!isLoading && children}
{isLoading && <span className='loading loading-dots loading-md' />}
</button>
)}
{href && (
<Link
href={disabled ? '#' : href}
aria-disabled={disabled}
className={cn(
btnBaseClassName,
{ 'pointer-events-auto cursor-not-allowed': disabled },
className
)}
>
{!isLoading && children}
{isLoading && <span className='loading loading-dots loading-md' />}
</Link>
)}
</>
);
};
export default Button;
+131
View File
@@ -0,0 +1,131 @@
import React, { useCallback, useId, useMemo, useState } from 'react';
import { cn } from '@/lib/helper';
export type CollapseVariant = 'default' | 'arrow' | 'plus';
export type CollapseProps = {
/** Unique name used when `asRadio` is true (Accordion single-open). */
name?: string;
/** If provided, component is controlled. */
open?: boolean;
/** Initial open state for uncontrolled usage. */
defaultOpen?: boolean;
/** Callback when open state changes. */
onOpenChange?: (open: boolean) => void;
/** Title row content. Accepts string or custom node. */
title?: React.ReactNode;
/** Optional secondary text displayed under/next to title. */
subtitle?: React.ReactNode;
/** Content of the panel. */
children?: React.ReactNode;
/** Visual variant: default / arrow / plus */
variant?: CollapseVariant;
/** Add a bordered look */
bordered?: boolean;
/** Disable interactions */
disabled?: boolean;
/** Allow only one open at a time by switching to radio input */
asRadio?: boolean;
/** Extra classnames */
className?: string;
titleClassName?: string;
contentClassName?: string;
};
export const Collapse = ({
name,
open,
defaultOpen,
onOpenChange,
title,
subtitle,
children,
variant = 'default',
bordered,
disabled,
asRadio = false,
className,
titleClassName,
contentClassName,
}: CollapseProps) => {
const inputId = useId();
const isControlled = typeof open === 'boolean';
const [internalOpen, setInternalOpen] = useState(!!defaultOpen);
const isOpen = isControlled ? !!open : internalOpen;
// Manage change from checkbox/radio
const handleChange = useCallback(
(next: boolean) => {
if (!isControlled) setInternalOpen(next);
onOpenChange?.(next);
},
[isControlled, onOpenChange]
);
const inputType = asRadio ? 'radio' : 'checkbox';
const rootClass = cn(
'collapse',
variant === 'arrow' && 'collapse-arrow',
variant === 'plus' && 'collapse-plus',
bordered && 'border base-content/20 border-opacity-20 rounded-box',
disabled && 'opacity-60 pointer-events-none',
!open && 'w-fit',
className
);
const titleNode = useMemo(() => {
if (subtitle) {
return (
<div className='flex flex-col gap-0.5'>
<span>{title}</span>
<span className='text-sm opacity-70'>{subtitle}</span>
</div>
);
}
return title;
}, [title, subtitle]);
return (
<div className={rootClass} data-open={isOpen}>
<input
id={inputId}
type={inputType}
name={asRadio ? name : undefined}
className='peer p-0 hidden'
checked={isOpen}
onChange={(e) => handleChange(e.currentTarget.checked)}
aria-controls={`${inputId}-content`}
disabled={disabled}
/>
<div
role='button'
tabIndex={0}
className={cn(
'collapse-title w-fit p-0',
'focus:outline-none focus-visible:ring focus-visible:ring-primary/40',
titleClassName
)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleChange(!isOpen);
}
}}
onClick={() => handleChange(!isOpen)}
>
{titleNode}
</div>
<div
id={`${inputId}-content`}
className={cn('collapse-content p-0!', contentClassName)}
>
{children}
</div>
</div>
);
};
export default Collapse;
+60
View File
@@ -0,0 +1,60 @@
'use client';
import { ReactNode } from 'react';
import { cn } from '@/lib/helper';
interface DrawerProps {
children?: ReactNode;
sidebarContent?: ReactNode;
open: boolean;
setOpen: (newOpenState: boolean) => void;
openOnLarge?: boolean;
}
const Drawer = ({
children,
sidebarContent,
open,
setOpen,
openOnLarge,
}: DrawerProps) => {
const toggleDrawer = () => {
setOpen(!open);
};
const closeDrawer = () => {
setOpen(false);
};
return (
<div
className={cn('drawer', {
'lg:drawer-open': openOnLarge,
})}
>
<input
type='checkbox'
checked={open}
onChange={toggleDrawer}
className='drawer-toggle'
/>
<div className='drawer-content'>{children}</div>
<div className='drawer-side border-r border-solid border-gray-200 z-20'>
<label
aria-label='close sidebar'
className='drawer-overlay'
onClick={closeDrawer}
/>
<div className='min-h-full w-full max-w-[300px] lg:w-[300px] bg-base-100'>
{sidebarContent}
</div>
</div>
</div>
);
};
export default Drawer;
+238
View File
@@ -0,0 +1,238 @@
'use client';
import { useCallback, useState } from 'react';
import { usePathname } from 'next/navigation';
import Image from 'next/image';
import { Icon } from '@iconify/react';
import Drawer from '@/components/Drawer';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import Navbar from '@/components/Navbar';
import Collapse from '@/components/Collapse';
import { useUiStore } from '@/stores/ui/ui.store';
import { MAIN_DRAWER_LINKS } from '@/config/constant';
import { cn } 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 = () => {
return (
<div className='w-full p-4 flex flex-col gap-4'>
<div className='flex items-center gap-4'>
<Image
src='/assets/img/lti-logo.png'
alt='MBU Logo'
width={256}
height={256}
className='w-full max-w-16 h-auto'
/>
<h1 className='text-xl font-bold'>LTI ERP</h1>
</div>
<MainDrawerMenu />
</div>
);
};
const MainDrawer = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
const { mainDrawerOpen, setMainDrawerOpen } = useUiStore();
const pathname = usePathname();
const getPageTitle = useCallback(() => {
let title = '';
const activeMenu = MAIN_DRAWER_LINKS.find((item) =>
isPathActive(pathname, item.link)
);
const traverseMenuTitle = (menu: typeof activeMenu) => {
if (!menu) return;
const hasSubmenu = menu?.submenu && menu?.submenu.length > 0;
if (!title) {
title += menu?.title;
} else {
title += ' - ' + menu?.title;
}
if (!hasSubmenu || !menu.submenu) return;
const activeSubmenu = menu.submenu?.find((item) =>
isPathActive(pathname, item.link)
);
traverseMenuTitle(activeSubmenu);
};
traverseMenuTitle(activeMenu);
return title;
}, [pathname]);
const pageTitle = getPageTitle();
const toggleSidebar = () => {
setMainDrawerOpen(!mainDrawerOpen);
};
return (
<Drawer
open={mainDrawerOpen}
setOpen={setMainDrawerOpen}
openOnLarge
sidebarContent={<MainDrawerContent />}
>
<main className='w-full h-full flex flex-col'>
<Navbar title={pageTitle as string} toggleSidebar={toggleSidebar} />
{children}
</main>
</Drawer>
);
};
export default MainDrawer;
+62
View File
@@ -0,0 +1,62 @@
'use client';
import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
import { cn } from '@/lib/helper';
export const useModal = () => {
const ref = useRef<HTMLDialogElement>(null);
const [open, setOpen] = useState(false);
const openModal = useCallback(() => {
setOpen(true);
ref.current?.showModal();
}, []);
const closeModal = useCallback(() => {
setOpen(false);
ref.current?.close();
}, []);
const toggle = useCallback(() => {
if (open) {
closeModal();
} else {
openModal();
}
}, [open, closeModal, openModal]);
if (ref.current) {
ref.current.addEventListener('close', () => {
closeModal();
});
}
return { ref, open, setOpen, openModal, closeModal, toggle } as const;
};
interface ModalProps {
ref: RefObject<HTMLDialogElement | null>;
children?: ReactNode;
closeOnBackdrop?: boolean;
className?: {
modal?: string;
modalBox?: string;
};
}
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
return (
<dialog ref={ref} className={cn('modal', className?.modal)}>
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
{closeOnBackdrop && (
<form method='dialog' className='modal-backdrop'>
<button>close</button>
</form>
)}
</dialog>
);
};
export default Modal;
+54
View File
@@ -0,0 +1,54 @@
'use client';
import { Icon } from '@iconify/react';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import Button from '@/components/Button';
interface NavbarProps {
title: string;
toggleSidebar?: () => void;
}
const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
return (
<div className='navbar px-4 bg-base-100 shadow-sm'>
<div className='flex-1'>
<div className='flex flex-row items-center gap-4'>
{toggleSidebar && (
<Button onClick={toggleSidebar} className='block lg:hidden'>
<Icon
icon='material-symbols:menu-rounded'
width={24}
height={24}
/>
</Button>
)}
<span className='font-bold text-xl text-primary'>{title}</span>
</div>
</div>
<div className='flex gap-2'>
<div className='dropdown dropdown-end'>
<div
tabIndex={0}
role='button'
className='btn btn-ghost btn-circle avatar'
>
<div className='w-10 rounded-full border grid place-items-center'>
<Icon icon='uil:user' width={40} height={40} />
</div>
</div>
<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' href='#' />
</Menu>
</div>
</div>
</div>
);
};
export default Navbar;
+335
View File
@@ -0,0 +1,335 @@
'use client';
import { ReactNode } from 'react';
import { Icon } from '@iconify/react';
import { cn } from '@/lib/helper';
const range = (start: number, end: number) =>
Array.from({ length: end - start + 1 }, (_, i) => i + start);
const PaginationButton = ({
content = '',
disabled = false,
onClick = () => {},
}: {
content?: ReactNode;
disabled?: boolean;
onClick?: () => void;
}) => (
<button
className={cn(
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square',
'disabled:text-gray-700 disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-gray-50 disabled:active:translate-y-0'
)}
disabled={disabled}
onClick={onClick}
>
{content}
</button>
);
const EtcPaginationButton = ({
startPage = 0,
endPage = 0,
onPageItemClick,
}: {
startPage: number;
endPage: number;
onPageItemClick: (a: number) => void;
}) => {
const pages = range(startPage, endPage);
return (
<>
{startPage > 0 && endPage >= startPage && (
<div className='dropdown dropdown-top dropdown-center'>
<button
tabIndex={0}
role='button'
className={cn(
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
)}
>
...
</button>
<div className='dropdown-content'>
<ul
tabIndex={0}
className='menu bg-base-100 rounded-lg z-1 w-fit min-w-max max-h-64 p-1 shadow-sm mb-2 overflow-y-auto flex-nowrap'
>
{pages.map((pageNumber) => (
<li key={pageNumber}>
<PaginationButton
content={pageNumber}
onClick={() => onPageItemClick(pageNumber)}
/>
</li>
))}
</ul>
</div>
</div>
)}
{(startPage === 0 || endPage < startPage) && (
<button
disabled
className={cn(
'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square'
)}
>
...
</button>
)}
</>
);
};
const Pagination = ({
currentPage = 1,
totalItems = 0,
itemsPerPage = 10,
onPageChange,
onPrevPage = () => {},
onNextPage = () => {},
}: {
currentPage: number;
totalItems: number;
itemsPerPage: number;
onPageChange: (pageNumber: number) => void;
onPrevPage: () => void;
onNextPage: () => void;
}) => {
const totalPages =
Math.ceil(totalItems / itemsPerPage) === 0
? 1
: Math.ceil(totalItems / itemsPerPage);
const pageChangeHandler = (pageNumber: number) => onPageChange(pageNumber);
return (
<div>
<div className='join w-full justify-between items-center gap-3'>
<button
disabled={currentPage === 1}
onClick={onPrevPage}
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-item join gap-0.5'>
{range(1, totalPages).map((pageNumber) => (
<PaginationButton
key={pageNumber}
content={pageNumber}
disabled={currentPage === pageNumber}
onClick={() => pageChangeHandler(pageNumber)}
/>
))}
</div>
)}
{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
content={totalPages}
disabled={currentPage === totalPages}
onClick={() => pageChangeHandler(totalPages)}
/>
)}
</div>
)}
<button
disabled={currentPage === totalPages}
onClick={onNextPage}
className={cn(
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs hidden sm:flex justify-center items-center gap-1.5',
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
Next{' '}
<Icon
icon='uil:arrow-right'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</button>
</div>
<div className='flex gap-2 mt-2 sm:hidden'>
<button
disabled={currentPage === 1}
onClick={onPrevPage}
className={cn(
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs flex justify-center items-center gap-1.5',
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
<Icon
icon='uil:arrow-left'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>{' '}
Previous
</button>
<button
disabled={currentPage === totalPages}
onClick={onNextPage}
className={cn(
'join-item btn btn-outline group px-3 py-2 text-sm font-semibold rounded-lg border border-gray-300 shadow-xs flex justify-center items-center gap-1.5',
'disabled:bg-[initial]! disabled:text-gray-400 disabled:pointer-events-auto! disabled:cursor-not-allowed disabled:active:translate-y-0'
)}
>
Next{' '}
<Icon
icon='uil:arrow-right'
width={20}
height={20}
className='text-gray-400 group-disabled:text-gray-300'
/>
</button>
</div>
</div>
);
};
export default Pagination;
+272
View File
@@ -0,0 +1,272 @@
'use client';
import { ReactNode, useCallback, useEffect, useState } from 'react';
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
TableOptions,
useReactTable,
ColumnDef,
FilterFn,
SortingState,
OnChangeFn,
} from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils';
import { Icon } from '@iconify/react';
import Pagination from '@/components/Pagination';
import { cn } from '@/lib/helper';
interface TableClassNames {
containerClassName?: string;
tableWrapperClassName?: string;
tableClassName?: string;
tableHeaderClassName?: string;
headerRowClassName?: string;
headerColumnClassName?: string;
tableBodyClassName?: string;
bodyRowClassName?: string;
bodyColumnClassName?: string;
paginationClassName?: string;
}
export interface TableProps<TData extends object> {
data: TData[];
columns: ColumnDef<TData, unknown>[];
pageSize?: number;
totalItems?: number;
page?: number;
onPageChange?: (page: number) => void;
isLoading?: boolean;
fuzzySearchValue?: string | null;
onFuzzySearchValueChange?: (value: string) => void;
className?: TableClassNames;
emptyContent?: ReactNode;
sorting?: SortingState;
setSorting?: OnChangeFn<SortingState>;
manualSorting?: boolean;
}
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
const emptyContentDefaultValue = (
<div className='w-full p-5 text-center'>
<span className='text-lg opacity-50'>
Tidak ada data yang dapat ditampilkan...
</span>
</div>
);
const Table = <TData extends object>({
data = [],
columns = [],
pageSize = 10,
totalItems,
page,
onPageChange,
isLoading = false,
fuzzySearchValue,
onFuzzySearchValueChange,
className = {
containerClassName: '',
tableWrapperClassName: '',
tableClassName: '',
tableHeaderClassName: '',
headerRowClassName: '',
headerColumnClassName: '',
tableBodyClassName: '',
bodyRowClassName: '',
bodyColumnClassName: '',
paginationClassName: '',
},
emptyContent = emptyContentDefaultValue,
sorting,
setSorting,
manualSorting = false,
}: TableProps<TData>) => {
const isServerSideTable =
totalItems !== undefined &&
page !== undefined &&
onPageChange !== undefined;
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: pageSize,
});
const fuzzyFilter: FilterFn<TData> = useCallback(
(row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value);
addMeta({ itemRank });
return itemRank.passed;
},
[]
);
const tableOptions: TableOptions<TData> = {
columns,
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
manualSorting,
state: {
pagination,
globalFilter: fuzzySearchValue,
},
filterFns: {
fuzzy: fuzzyFilter,
},
globalFilterFn: fuzzyFilter,
};
if (fuzzySearchValue !== null) {
tableOptions.onGlobalFilterChange = onFuzzySearchValueChange;
tableOptions.getFilteredRowModel = getFilteredRowModel();
}
if (sorting && setSorting) {
tableOptions.onSortingChange = setSorting;
tableOptions.state = {
...tableOptions.state,
sorting,
};
}
const table = useReactTable(tableOptions);
const { setPageSize } = table;
const prevPageClickHandler = () => {
table.previousPage();
if (isServerSideTable) {
onPageChange(page - 1);
}
};
const nextPageClickHandler = () => {
table.nextPage();
if (isServerSideTable) {
onPageChange(page + 1);
}
};
const pageChangeHandler = (pageNumber: number) => {
const currentPage = pageNumber - 1;
table.setPageIndex(pageNumber ? currentPage : 0);
if (isServerSideTable) {
onPageChange(pageNumber);
}
};
useEffect(() => {
setPageSize(pageSize);
}, [pageSize, setPageSize]);
return (
<div className={className.containerClassName}>
<div className={className.tableWrapperClassName}>
<table className={className.tableClassName}>
<thead className={className.tableHeaderClassName}>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className={className.headerRowClassName}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
colSpan={header.colSpan}
onClick={header.column.getToggleSortingHandler()}
className={cn(
header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
className.headerColumnClassName
)}
>
<div className='flex items-center gap-1'>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanSort() && (
<div className='flex items-center'>
<Icon
icon='lucide:arrow-up'
width={12}
height={12}
className={cn(
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc'
? 'text-black'
: 'text-black/30'
)}
/>
<Icon
icon='lucide:arrow-down'
width={12}
height={12}
className={cn(
'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc'
? 'text-black'
: 'text-black/30'
)}
/>
</div>
)}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody className={className.tableBodyClassName}>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className={className.bodyRowClassName}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className={className.bodyColumnClassName}>
{!isLoading &&
flexRender(cell.column.columnDef.cell, cell.getContext())}
{isLoading && <div className='skeleton w-full h-4' />}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
!isLoading &&
emptyContent}
{data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && (
<div className={cn('mt-5', className.paginationClassName)}>
<Pagination
totalItems={isServerSideTable ? totalItems : table.getRowCount()}
itemsPerPage={table.getState().pagination.pageSize}
currentPage={
isServerSideTable
? page
: table.getState().pagination.pageIndex + 1
}
onPrevPage={prevPageClickHandler}
onNextPage={nextPageClickHandler}
onPageChange={pageChangeHandler}
/>
</div>
)}
</div>
);
};
export default Table;
+197
View File
@@ -0,0 +1,197 @@
'use client';
import { ReactNode, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseSuccess } from '@/lib/api-helper';
import { GetMeResponse } from '@/types/api/api-general';
// TODO: delete this later, DONT HARDCODE USER DATA
const DUMMY_USER = {
id: 1,
email: 'admin@mbugroup.id',
npk: '0001',
name: 'Super Admin',
image: null,
created_at: '2025-09-30T03:24:20.899229Z',
updated_at: '2025-09-30T03:24:20.899229Z',
roles: [
{
id: 1,
key: 'mbu.super_admin',
name: 'MBU Administrator',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
permissions: [
{
id: 1,
name: 'mbu:purchase:read',
action: 'read',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 2,
name: 'mbu:purchase:create',
action: 'create',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 3,
name: 'mbu:purchase:approve',
action: 'approve',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
],
},
{
id: 2,
key: 'lti.super_admin',
name: 'LTI Administrator',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
permissions: [
{
id: 4,
name: 'lti:purchase:read',
action: 'read',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 5,
name: 'lti:purchase:create',
action: 'create',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 6,
name: 'lti:purchase:approve',
action: 'approve',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
],
},
{
id: 3,
key: 'manbu.super_admin',
name: 'MANBU Administrator',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
permissions: [
{
id: 7,
name: 'manbu:purchase:read',
action: 'read',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 8,
name: 'manbu:purchase:create',
action: 'create',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 9,
name: 'manbu:purchase:approve',
action: 'approve',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
],
},
],
};
interface RequireAuthProps {
children?: ReactNode;
}
const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter();
const { setUser, setIsLoadingUser } = useAuth();
const { data: userResponse, isLoading: isLoadingUserResponse } =
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
'/auth/get-me',
httpClientFetcher,
{
shouldRetryOnError: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 0,
}
);
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse, setIsLoadingUser]);
useEffect(() => {
if (isResponseSuccess(userResponse)) {
setUser(userResponse.data);
} else {
// router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
// TODO: remove this later, DONT HARDCODE USER DATA
setUser(DUMMY_USER);
}
}, [userResponse, setIsLoadingUser, setUser]);
// TODO: uncomment this later
// if (isLoadingUserResponse && !userResponse) {
// return (
// <div className='w-full flex flex-row justify-center items-center p-4'>
// <span className='loading loading-spinner loading-xl' />
// </div>
// );
// }
return <>{children}</>;
};
export default RequireAuth;
+23
View File
@@ -0,0 +1,23 @@
'use client';
import { Suspense } from 'react';
const SuspenseHelper = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return (
<Suspense
fallback={
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
}
>
{children}
</Suspense>
);
};
export default SuspenseHelper;
@@ -0,0 +1,42 @@
'use client';
import { ChangeEvent, ChangeEventHandler, useEffect, useState } from 'react';
import { useDebounce } from 'use-debounce';
import TextInput, { TextInputProps } from '@/components/input/TextInput';
interface DebouncedTextInputProps extends TextInputProps {
delay?: number;
}
const DebouncedTextInput = (props: DebouncedTextInputProps) => {
const { delay, onChange } = props;
const [internalChangeEvent, setInternalChangeEvent] =
useState<ChangeEvent<HTMLInputElement>>();
const [internalValue, setInternalValue] = useState(props.value);
const [debouncedChangeEvent] = useDebounce(internalChangeEvent, delay ?? 300);
const [debouncedValue] = useDebounce(internalValue, delay ?? 300);
const internalChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setInternalValue(e.target.value);
setInternalChangeEvent(e);
};
useEffect(() => {
if (debouncedChangeEvent) {
onChange?.(debouncedChangeEvent);
}
}, [debouncedValue]);
return (
<TextInput
{...props}
value={internalValue}
onChange={internalChangeHandler}
/>
);
};
export default DebouncedTextInput;
+88
View File
@@ -0,0 +1,88 @@
import { Ref } from 'react';
import { cn } from '@/lib/helper';
import { TextInputProps } from '@/components/input/TextInput';
interface FileInputProps
extends Omit<
TextInputProps,
| 'type'
| 'value'
| 'isValid'
| 'startAdornment'
| 'endAdornment'
| 'isLoading'
> {
ref?: Ref<HTMLInputElement>;
accept?: string;
className?: {
wrapper?: string;
label?: string;
input?: string;
};
}
const FileInput = ({
ref,
label,
bottomLabel,
name,
placeholder,
accept = '*',
className,
isError,
errorMessage,
disabled = false,
onChange,
onBlur,
readOnly = false,
}: FileInputProps) => {
return (
<div
className={cn(
'w-full flex flex-col gap-2 text-start',
className?.wrapper
)}
>
{label && (
<label
htmlFor={name}
className={cn(
'w-full text-sm font-normal leading-5',
{
'text-error': isError,
},
className?.label
)}
>
{label}
</label>
)}
<input
ref={ref}
type='file'
accept={accept}
id={name}
name={name}
placeholder={placeholder}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn(
'grow file-input w-full h-12 rounded-lg!',
className?.input
)}
readOnly={readOnly}
/>
{bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
)}
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
</div>
);
};
export default FileInput;
+49
View File
@@ -0,0 +1,49 @@
'use client';
import { useState } from 'react';
import { Icon } from '@iconify/react';
import TextInput, { TextInputProps } from '@/components/input/TextInput';
import Button from '@/components/Button';
type PasswordInputProps = Omit<
TextInputProps,
'type' | 'startAdornment' | 'endAdornment'
>;
const PasswordInput = (props: PasswordInputProps) => {
const [type, setType] = useState('password');
const showPasswordHandler = () => {
setType((prevType) => {
if (prevType === 'password') return 'text';
return 'password';
});
};
return (
<TextInput
{...props}
type={type}
endAdornment={
<Button
tabIndex={-1}
type='button'
variant='ghost'
onClick={showPasswordHandler}
className='btn btn-ghost w-fit h-fit p-2 rounded-full'
disabled={props.disabled}
>
<Icon
icon={type === 'password' ? 'mdi:eye' : 'mdi:eye-off'}
width={16}
height={16}
/>
</Button>
}
/>
);
};
export default PasswordInput;
+113
View File
@@ -0,0 +1,113 @@
'use client';
import { ChangeEventHandler, ReactNode } from 'react';
import { cn } from '@/lib/helper';
export interface RadioOption {
label: string;
value: string;
}
export interface RadioInputProps {
label?: string;
bottomLabel?: string;
name: string;
value?: string;
options: RadioOption[];
variant?: string;
className?: {
wrapper?: string;
label?: string;
radioWrapper?: string;
radio?: string;
};
isError?: boolean;
isValid?: boolean;
errorMessage?: string;
required?: boolean;
disabled?: boolean;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
}
const RadioInput = ({
label,
bottomLabel,
name,
value,
options,
variant = 'radio-primary',
className,
isError,
errorMessage,
required = false,
disabled = false,
onChange,
onBlur,
}: RadioInputProps) => {
return (
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
{/* Label atas */}
{label && (
<label
className={cn(
'w-full text-sm font-normal leading-5',
{ 'text-error': isError },
className?.label
)}
>
{label}
{required && (
<span className='text-error ml-1' title='required'>
*
</span>
)}
</label>
)}
{/* Daftar opsi radio */}
<div
className={cn(
'flex flex-row flex-wrap gap-4 items-center',
className?.radioWrapper
)}
>
{options.map((option) => (
<label
key={option.value}
className={cn(
'flex flex-row items-center gap-2 cursor-pointer',
disabled && 'opacity-60 cursor-not-allowed'
)}
>
<input
type='radio'
name={name}
value={option.value}
checked={value === option.value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('radio', variant, className?.radio)}
/>
<span className='text-sm'>{option.label}</span>
</label>
))}
</div>
{/* Label bawah */}
{!isError && bottomLabel && (
<p className='text-sm opacity-60'>{bottomLabel}</p>
)}
{/* Pesan error */}
{isError && errorMessage && (
<p className='text-sm text-error'>{errorMessage}</p>
)}
</div>
);
};
export default RadioInput;
+236
View File
@@ -0,0 +1,236 @@
'use client';
import {
ComponentType,
ReactNode,
useEffect,
useMemo,
useState,
} from 'react';
import Select, {
OptionProps,
GroupBase,
InputActionMeta,
MultiValue,
SingleValue,
} from 'react-select';
import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated';
import { useDebounce } from 'use-debounce';
import { cn } from '@/lib/helper';
export interface OptionType {
value: string | number;
label: string;
className?: string;
labelClassName?: string;
}
export type OptionComponent<T = OptionType> = ComponentType<
OptionProps<T, boolean, GroupBase<T>>
>;
interface SelectInputBaseProps<T = OptionType> {
label?: ReactNode;
bottomLabel?: ReactNode;
options: T[];
optionComponent?: OptionComponent<T>;
isDisabled?: boolean;
isLoading?: boolean;
isClearable?: boolean;
isRtl?: boolean;
isSearchable?: boolean;
isMulti?: boolean;
placeholder?: string;
required?: boolean;
className?: {
wrapper?: string;
label?: string;
select?: string;
};
isError?: boolean;
errorMessage?: string;
isAnimated?: boolean;
openMenu?: boolean;
delay?: number;
onInputChange?: (search: string) => void;
}
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
createables?: boolean;
value?: T | T[] | null;
onChange?: (val: T | T[] | null) => void;
}
const animatedComponents = makeAnimated();
const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const {
label,
bottomLabel,
value,
onChange,
options,
optionComponent,
isDisabled,
isLoading,
isClearable,
isRtl,
isSearchable = true,
isMulti,
placeholder,
required,
className,
isError,
errorMessage,
isAnimated = true,
openMenu,
delay = 300,
createables = false,
onInputChange,
} = props;
const [internalInputValue, setInternalInputValue] = useState('');
const [debouncedInputValue] = useDebounce(internalInputValue, delay);
const components = useMemo(() => {
const base = isAnimated ? animatedComponents : {};
return { ...base, IndicatorSeparator: () => null };
}, [isAnimated]);
const internalInputChangeHandler = (
val: string,
meta: InputActionMeta
) => {
if (meta.action === 'input-change') setInternalInputValue(val);
if (meta.action === 'menu-close') setInternalInputValue('');
};
useEffect(() => {
onInputChange?.(debouncedInputValue);
}, [onInputChange, debouncedInputValue]);
const SelectComponent = createables ? CreatableSelect : Select;
/** 🎯 handleChange tanpa any */
const handleChange = (
val: MultiValue<T> | SingleValue<T>
): void => {
if (!val) {
onChange?.(null);
return;
}
if (isMulti) {
onChange?.(val as T[]);
} else {
onChange?.(val as T);
}
};
return (
<div
className={cn(
'w-full flex flex-col gap-2 text-start',
className?.wrapper
)}
>
{label && (
<span
className={cn(
'w-full text-sm font-normal leading-5',
{ 'text-error': isError },
className?.label
)}
>
{label}
{required && (
<span className="tooltip tooltip-error" data-tip="required">
<span className="text-error"> *</span>
</span>
)}
</span>
)}
<SelectComponent<T, boolean, GroupBase<T>>
instanceId="select"
value={value ?? (isMulti ? [] : null)}
onChange={handleChange}
options={options}
menuIsOpen={openMenu}
inputValue={internalInputValue}
onInputChange={internalInputChangeHandler}
isMulti={isMulti}
isDisabled={isDisabled}
isLoading={isLoading}
isClearable={isClearable}
isRtl={isRtl}
isSearchable={isSearchable}
placeholder={placeholder}
className={cn('w-full', className?.select)}
classNames={{
control: ({ isFocused, isDisabled }) =>
cn(
'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer!',
{
'border-red-500! ring-2 ring-red-200': isError,
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
'border-gray-300': !isError && !isFocused,
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
}
),
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
placeholder: () =>
cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
singleValue: () =>
cn({ 'text-gray-900': !isError, 'text-error!': isError }),
input: () => cn('text-gray-900'),
indicatorsContainer: () => cn('flex items-center gap-1 pr-2'),
dropdownIndicator: ({ isFocused }) =>
cn('p-1 rounded-md hover:bg-gray-100', {
'text-gray-900': isFocused,
'text-gray-500': !isFocused,
'text-error!': isError,
}),
menu: () =>
cn('border border-gray-200 rounded-lg bg-white shadow-lg!'),
menuList: () => cn('p-2! max-h-60 overflow-auto'),
option: ({ isFocused, isSelected }) =>
cn('mt-1 px-3 py-2 rounded-md cursor-pointer!', {
'bg-indigo-600 text-white': isFocused,
'bg-blue-500!': isSelected,
'text-gray-700': !isFocused && !isSelected,
}),
multiValue: ({ getValue, index }) => {
const selectedValues = getValue() as T[];
return cn(
'bg-indigo-50 rounded-md py-0.5 pl-2 pr-1 flex items-center gap-1!',
selectedValues[index]?.className
);
},
multiValueLabel: ({ getValue, index }) => {
const selectedValues = getValue() as T[];
return cn('text-indigo-700', selectedValues[index]?.labelClassName);
},
}}
components={{
...components,
...(optionComponent ? { Option: optionComponent } : {}),
}}
menuPortalTarget={
typeof document !== 'undefined' ? document.body : undefined
}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
}}
/>
{isError && <p className="w-full text-sm text-error">{errorMessage}</p>}
{!isError && bottomLabel && (
<p className="w-full text-sm opacity-60">{bottomLabel}</p>
)}
</div>
);
};
export default SelectInput;
+169
View File
@@ -0,0 +1,169 @@
'use client';
import React, { useState, KeyboardEvent, ChangeEvent, useEffect } from 'react';
import { cn } from '@/lib/helper';
export interface TagInputProps {
label?: string;
bottomLabel?: string;
name: string;
value?: string;
placeholder?: string;
className?: {
wrapper?: string;
label?: string;
inputWrapper?: string;
input?: string;
};
isError?: boolean;
isValid?: boolean;
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
isLoading?: boolean;
errorMessage?: string;
onChange?: (value: string) => void;
}
const TagInput: React.FC<TagInputProps> = ({
label,
bottomLabel,
name,
value = '',
placeholder,
className,
isError,
isValid,
errorMessage,
disabled = false,
readOnly = false,
required = false,
onChange,
}) => {
const [tags, setTags] = useState<string[]>(value ? value.split(',') : []);
const [inputValue, setInputValue] = useState('');
useEffect(() => {
if (value !== undefined && value !== tags.join(',')) {
setTags(value ? value.split(',') : []);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
const newTag = inputValue.trim();
if (newTag && !tags.includes(newTag)) {
const updatedTags = [...tags, newTag];
setTags(updatedTags);
onChange?.(updatedTags.join(','));
}
setInputValue('');
}
};
const handleRemoveTag = (tagToRemove: string) => {
const updatedTags = tags.filter((t) => t !== tagToRemove);
setTags(updatedTags);
onChange?.(updatedTags.join(','));
};
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
return (
<div
className={cn(
'w-full flex flex-col gap-2 text-start',
className?.wrapper
)}
>
{/* Label */}
{label && (
<label
htmlFor={name}
className={cn(
'w-full text-sm font-normal leading-5',
{ 'text-error': isError },
className?.label
)}
>
{label}
{required && (
<>
{' '}
<span className='tooltip tooltip-error' data-tip='required'>
<span className='text-error'> *</span>
</span>
</>
)}
</label>
)}
{/* Input wrapper */}
<div
className={cn(
'flex flex-wrap items-start gap-2 border border-gray-400 rounded-md p-2 focus-within:ring-2 focus-within:ring-blue-500 min-h-[42px] transition-all',
{
'border-error': isError,
'border-success!': isValid,
'opacity-70 cursor-not-allowed': disabled,
},
className?.inputWrapper
)}
onClick={() => {
// Fokuskan input saat area diklik
const inputEl = document.getElementById(name);
inputEl?.focus();
}}
>
{tags.map((tag) => (
<div
key={tag}
className={cn(
'badge badge-primary gap-1 px-3 py-3 text-white flex items-center'
)}
>
<span>{tag}</span>
{!readOnly && (
<button
type='button'
onClick={() => handleRemoveTag(tag)}
className='ml-1 text-white hover:text-red-200 focus:outline-none'
>
</button>
)}
</div>
))}
{!readOnly && (
<input
type='text'
id={name}
name={name}
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={disabled}
className={cn(
'flex-1 min-w-[120px] border-none outline-none p-1 size-min',
className?.input
)}
/>
)}
</div>
{/* Bottom label or error message */}
{!isError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
)}
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
</div>
);
};
export default TagInput;
+124
View File
@@ -0,0 +1,124 @@
'use client';
import {
ChangeEventHandler,
FocusEventHandler,
ReactNode,
} from 'react';
import { cn } from '@/lib/helper';
export interface TextAreaProps {
label?: string;
bottomLabel?: string;
name: string;
value?: string | number;
placeholder?: string;
className?: {
wrapper?: string;
label?: string;
inputWrapper?: string;
input?: string;
};
isError?: boolean;
isValid?: boolean;
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
isLoading?: boolean;
errorMessage?: string;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
rows?: number;
}
const TextArea = ({
label,
bottomLabel,
name,
value,
placeholder,
className,
isError,
isValid,
errorMessage,
startAdornment,
endAdornment,
disabled = false,
required = false,
onChange,
onBlur,
readOnly = false,
isLoading = false,
rows = 3
}: TextAreaProps) => {
return (
<div
className={cn(
'w-full flex flex-col gap-2 text-start',
className?.wrapper
)}
>
{label && (
<label
htmlFor={name}
className={cn(
'w-full text-sm font-normal leading-5',
{
'text-error': isError,
},
className?.label
)}
>
{label}
{required && (
<>
{' '}
<span className='tooltip tooltip-error' data-tip='required'>
<span className='text-error'> *</span>
</span>
</>
)}
</label>
)}
{startAdornment && startAdornment}
<textarea
className={cn(
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all',
{
'border-error': isError,
'border-success!': isValid,
},
className?.inputWrapper
)}
id={name}
name={name}
placeholder={placeholder}
value={value}
rows={rows}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
readOnly={readOnly}
/>
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
{!isError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
)}
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
</div>
);
};
export default TextArea;
+132
View File
@@ -0,0 +1,132 @@
'use client';
import {
ChangeEventHandler,
FocusEventHandler,
HTMLInputTypeAttribute,
ReactNode,
} from 'react';
import { cn } from '@/lib/helper';
export interface TextInputProps {
type?: HTMLInputTypeAttribute;
label?: string;
bottomLabel?: string;
name: string;
value?: string | number;
placeholder?: string;
className?: {
wrapper?: string;
label?: string;
inputWrapper?: string;
input?: string;
};
isError?: boolean;
isValid?: boolean;
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
isLoading?: boolean;
errorMessage?: string;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
}
const TextInput = ({
type = 'text',
label,
bottomLabel,
name,
value,
placeholder,
className,
isError,
isValid,
errorMessage,
startAdornment,
endAdornment,
disabled = false,
required = false,
onChange,
onBlur,
readOnly = false,
isLoading = false,
}: TextInputProps) => {
return (
<div
className={cn(
'w-full flex flex-col gap-2 text-start',
className?.wrapper
)}
>
{label && (
<label
htmlFor={name}
className={cn(
'w-full text-sm font-normal leading-5',
{
'text-error': isError,
},
className?.label
)}
>
{label}
{required && (
<>
{' '}
<span className='tooltip tooltip-error' data-tip='required'>
<span className='text-error'> *</span>
</span>
</>
)}
</label>
)}
<div
className={cn(
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200',
{
'border-error': isError,
'border-success!': isValid,
},
className?.inputWrapper
)}
>
{startAdornment && startAdornment}
<input
type={type}
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('grow', className?.input)}
readOnly={readOnly}
/>
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
</div>
{!isError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
)}
{isError && errorMessage && (
<p className='w-full text-sm text-error'>{errorMessage}</p>
)}
</div>
);
};
export default TextInput;
+16
View File
@@ -0,0 +1,16 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/helper';
interface MenuProps {
children?: ReactNode;
className?: string;
}
const Menu = ({ children, className }: MenuProps) => {
return (
<ul className={cn('menu w-full p-0 gap-0.5', className)}>{children}</ul>
);
};
export default Menu;
+64
View File
@@ -0,0 +1,64 @@
import Link from 'next/link';
import { Icon } from '@iconify/react';
import { cn } from '@/lib/helper';
interface MenuItemProps {
title: string;
href?: string;
icon?: string;
active?: boolean;
onClick?: () => void;
className?: string;
}
const MenuItem = ({
title,
href,
icon,
active = false,
className,
onClick,
}: MenuItemProps) => {
const menuItemBaseClassName = cn(
'group px-3 py-2 text-base text-black font-semibold flex flex-row items-center rounded-md',
{ 'bg-gray-100 border-l-2 border-l-primary': active },
className
);
const menuItemContent = (
<>
{icon && (
<Icon
icon={icon}
width={20}
height={20}
className={cn('group-active:text-[inherit]', {
'text-gray-400': !active,
'text-black': active,
})}
/>
)}
<span
className={cn({ 'opacity-40': !active }, 'group-active:opacity-100')}
>
{title}
</span>
</>
);
return (
<li onClick={onClick}>
{href && (
<Link href={href} className={menuItemBaseClassName}>
{menuItemContent}
</Link>
)}
{!href && <a className={menuItemBaseClassName}>{menuItemContent}</a>}
</li>
);
};
export default MenuItem;
+124
View File
@@ -0,0 +1,124 @@
'use client';
import { RefObject } from 'react';
import { Icon } from '@iconify/react';
import Modal from '@/components/Modal';
import Button from '@/components/Button';
import { cn } from '@/lib/helper';
import { Color } from '@/types/theme';
interface ConfirmationModalProps {
ref: RefObject<HTMLDialogElement | null>;
type?: 'info' | 'success' | 'error';
text?: string;
closeOnBackdrop?: boolean;
primaryButton?: {
text?: string;
color?: Color;
isLoading?: boolean;
onClick?: () => void;
};
secondaryButton?: {
text?: string;
color?: Color;
isLoading?: boolean;
onClick?: () => void;
};
className?: {
modal?: string;
modalBox?: string;
};
}
const ConfirmationModal = ({
ref,
type = 'info',
text,
closeOnBackdrop,
primaryButton,
secondaryButton,
className,
}: ConfirmationModalProps) => {
const closeModalHandler = () => {
ref.current?.close();
};
return (
<Modal ref={ref} closeOnBackdrop={closeOnBackdrop} className={className}>
<div className='w-full flex flex-col gap-4'>
<div
className={cn(
'w-fit p-4 mx-auto flex flex-row justify-center items-center rounded-full',
{
'bg-error': type === 'error',
'bg-info': type === 'info',
'bg-success': type === 'success',
}
)}
>
{type === 'info' && (
<Icon
icon='material-symbols:info-outline-rounded'
width={64}
height={64}
className='text-info-content'
/>
)}
{type === 'success' && (
<Icon
icon='qlementine-icons:success-12'
width={64}
height={64}
className='text-success-content'
/>
)}
{type === 'error' && (
<Icon
icon='solar:danger-triangle-linear'
width={64}
height={64}
className='text-error-content'
/>
)}
</div>
<p className='text-center font-medium'>
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
</p>
<div className='w-full flex flex-row gap-2'>
{secondaryButton && secondaryButton.text && (
<Button
variant='ghost'
color={secondaryButton?.color ?? 'none'}
isLoading={secondaryButton?.isLoading}
disabled={secondaryButton?.isLoading}
onClick={closeModalHandler}
className='grow'
>
{secondaryButton?.text ?? 'Tidak'}
</Button>
)}
{primaryButton && primaryButton.text && (
<Button
color={primaryButton?.color ?? 'info'}
onClick={primaryButton?.onClick}
isLoading={primaryButton?.isLoading}
disabled={primaryButton?.isLoading}
className='grow'
>
{primaryButton?.text ?? 'Ya'}
</Button>
)}
</div>
</div>
</Modal>
);
};
export default ConfirmationModal;
@@ -0,0 +1,263 @@
'use client';
import Button from '@/components/Button';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import Table from '@/components/Table';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { inventoryAdjustmentApi } from '@/services/api/inventory';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
import { Icon } from '@iconify/react';
import {
ColumnDef,
ColumnSort,
SortingState,
} from '@tanstack/react-table';
import { useCallback, useEffect, useState } from 'react';
import useSWR from 'swr';
const InventoryAdjustmentTable = () => {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
productCategorySort: '',
productSort: '',
warehouseSort: '',
stockSort: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
productCategorySort: 'sort_product_category',
productSort: 'sort_product',
warehouseSort: 'sort_warehouse',
stockSort: 'sort_stock',
},
});
// Fetch Data
const {
data: inventoryAdjustments,
isLoading,
} = useSWR(
`${inventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
inventoryAdjustmentApi.getAllFetcher
);
// State
const [sorting, setSorting] = useState<SortingState>([]);
// Columns
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = [
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
id: 'product_name',
header: 'Nama Produk',
accessorFn: (row) => row.product_warehouse?.product?.name ?? '-',
},
{
id: 'warehouse_name',
header: 'Gudang',
accessorFn: (row) => row.product_warehouse?.warehouse?.name ?? '-',
},
{
id: 'created_at',
header: 'Tanggal',
accessorFn: (row) =>
new Date(row.created_at).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
}),
},
{
id: 'before_quantity',
header: 'Stok Sebelum',
accessorFn: (row) => formatNumber(String(row.before_quantity)),
},
{
id: 'after_quantity',
header: 'Stok Sesudah',
accessorFn: (row) => formatNumber(String(row.after_quantity)),
},
{
id: 'quantity',
header: 'Kuantitas',
accessorFn: (row) => formatNumber(String(row.quantity)),
},
{
id: 'transaction_type',
header: 'Tipe Transaksi',
accessorFn: (row) => {
if (row.transaction_type === 'INCREASE') return 'Peningkatan';
if (row.transaction_type === 'DECREASE') return 'Penurunan';
return '-';
},
cell: (props) => {
const type = props.row.original.transaction_type;
const label =
type === 'INCREASE'
? 'Peningkatan'
: type === 'DECREASE'
? 'Penurunan'
: '-';
return (
<div
className={`small mx-auto badge badge-soft ${
type === 'INCREASE' ? 'badge-success' : 'badge-error'
}`}
>
{label}
</div>
);
},
},
{
id: 'created_by',
header: 'Oleh',
accessorFn: (row) => row.created_user?.name ?? '-',
},
];
// Handler
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
};
const updateSortingFilter = useCallback(
(
sortName: Exclude<keyof typeof tableFilterState, 'page' | 'pageSize'>,
sortFilter: ColumnSort | undefined
) => {
if (!sortFilter) {
updateFilter(sortName, '');
} else {
updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc');
}
},
[updateFilter]
);
// Effect
useEffect(() => {
const productCategorySortFilter = sorting.find(
(sortItem) => sortItem.id === 'productCategory'
);
const productSortFilter = sorting.find(
(sortItem) => sortItem.id === 'product'
);
const warehouseSortFilter = sorting.find(
(sortItem) => sortItem.id === 'warehouse'
);
const stockSortFilter = sorting.find((sortItem) => sortItem.id === 'stock');
updateSortingFilter('productCategorySort', productCategorySortFilter);
updateSortingFilter('productSort', productSortFilter);
updateSortingFilter('warehouseSort', warehouseSortFilter);
updateSortingFilter('stockSort', stockSortFilter);
}, [sorting, updateSortingFilter]);
// Utils Function
const formatNumber = (value: string) => {
const numericValue = value.replace(/[^0-9.]/g, '');
const [integer, decimal] = numericValue.split('.');
const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return decimal ? `${formattedInteger}.${decimal}` : formattedInteger;
};
// Render
return (
<>
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='flex flex-row'>
<Button href='/inventory/adjustment/add' color='primary'>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
{/* <DebouncedTextInput
name='search'
placeholder='Cari Stock Adjustment'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/> */}
</div>
<div className='flex flex-row justify-end'>
<SelectInput
label='Baris'
options={ROWS_OPTIONS}
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/>
</div>
</div>
<Table<InventoryAdjustment>
data={
isResponseSuccess(inventoryAdjustments)
? inventoryAdjustments?.data
: []
}
columns={inventoryAdjustmentsColumns}
pageSize={tableFilterState.pageSize}
page={
isResponseSuccess(inventoryAdjustments)
? inventoryAdjustments?.meta?.page
: 0
}
totalItems={
isResponseSuccess(inventoryAdjustments)
? inventoryAdjustments?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(inventoryAdjustments) &&
inventoryAdjustments?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
</div>
</div>
</>
);
};
export default InventoryAdjustmentTable;

Some files were not shown because too many files have changed in this diff Show More