Compare commits

...

255 Commits

Author SHA1 Message Date
Adnan Zahir ca58e19a48 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!331
2026-02-23 09:32:40 +07:00
Rivaldi A N S ac2d83a666 Merge branch 'fix/cleanup-code' into 'development'
[FIX/FE] Partial Remove Issue Warning on Build (1/2)

See merge request mbugroup/lti-web-client!330
2026-02-23 02:25:50 +00:00
rstubryan 03e0cebe35 refactor(FE): Fix formatting for array dependencies and className
conditions
2026-02-20 14:42:46 +07:00
rstubryan 1cc0e16c01 refactor(FE): Replace ButtonFilter with custom Filter button 2026-02-20 14:41:50 +07:00
rstubryan 1f2f3acebb refactor(FE): Remove unused imports and redundant code 2026-02-20 14:17:26 +07:00
Rivaldi A N S de0f9ae985 Merge branch 'fix/uniformity-ttl-project-flock-issue' into 'development'
[FIX/FE] Fix Isssue on Uniformity (Week Calculation), Project Flock (Modal Position), Transfer To Laying (Copywriting)

See merge request mbugroup/lti-web-client!329
2026-02-20 03:36:15 +00:00
rstubryan a0e79168b2 refactor(FE): Refactor setClosingLoading to use single-line syntax 2026-02-20 10:25:30 +07:00
rstubryan 797f88fe15 feat(FE): Add project flock closing modal and zustand store 2026-02-20 10:23:47 +07:00
rstubryan 4c3e7c615f refactor(FE): Refactor conditional text formatting in modal component 2026-02-20 10:12:06 +07:00
rstubryan b35b6c2ab8 feat(FE): Add submittedActionType state to track modal action type 2026-02-20 10:11:16 +07:00
Adnan Zahir 0971e6ddeb Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!328
2026-02-20 09:53:46 +07:00
rstubryan bbbd767cf2 refactor(FE): Fix week calculation logic in UniformityForm 2026-02-20 09:21:42 +07:00
Rivaldi A N S 3e30dcb04e Merge branch 'fix/closing-project-flock-recording-issue' into 'development'
[FIX/FE] Adjustment Closing Related (Closing Report, Closing Detail and Closing Table), Project Flock Detail (Form, Chickin), Recording Table

See merge request mbugroup/lti-web-client!327
2026-02-19 09:45:18 +00:00
rstubryan 1a137e7500 refactor(FE): Normalize status keys to uppercase in status utilities 2026-02-19 15:37:46 +07:00
rstubryan 3be6d5bb26 refactor(FE): Update status mappings for "CREATED" to "Pengajuan" 2026-02-19 15:35:08 +07:00
rstubryan e22f95cc58 refactor(FE): Remove unused variable approval.step_name in
RecordingTable
2026-02-19 15:28:27 +07:00
rstubryan 6ac903313c refactor(FE): Refactor chickin approval modal logic into Zustand store 2026-02-19 15:22:28 +07:00
rstubryan a4ff92520a refactor(FE): Update toast message for Project Flock approval/rejection 2026-02-19 14:50:43 +07:00
rstubryan 608cf4cbe7 feat(FE): Add StatusBadge to display project status in ClosingsTable 2026-02-19 14:41:21 +07:00
rstubryan 60e360537e refactor(FE): Refactor ClosingsTable header for improved styling 2026-02-19 14:37:20 +07:00
rstubryan e9784bd5ed refactor(FE): Refactor ClosingsTable component and update styles 2026-02-19 14:33:35 +07:00
rstubryan 4f018eb2b1 feat(FE): Add filter modal and skeleton components for ClosingsTable 2026-02-19 14:18:22 +07:00
rstubryan 40b3d779bc refactor(FE): Update column header and adjust table margin 2026-02-19 13:52:32 +07:00
rstubryan 1bdf413650 feat(FE): Add badge display for category columns in tables 2026-02-19 13:33:11 +07:00
rstubryan 495b1b2869 refactor(FE):Add HPP Amount column to daily marketing export 2026-02-19 12:18:27 +07:00
rstubryan a231140bc0 refactor(FE): Refactor ProductionDataClosingTab to use Card component 2026-02-19 12:02:04 +07:00
rstubryan a0af934002 feat(FE): Add headings and improve layout for financial tables 2026-02-19 11:52:57 +07:00
rstubryan 82975219a8 refactor(FE): Update border styles for ClosingDetailTabs and
ClosingKandangList
2026-02-19 11:43:46 +07:00
rstubryan 60ae670f24 refactor(FE): Refactor UI and improve conditional rendering in closing
pages
2026-02-19 11:38:34 +07:00
rstubryan 7d79b6b957 refactor(FE): Refactor table and tab styles for consistent spacing and
layout
2026-02-19 11:27:24 +07:00
rstubryan 8a1e0f080f refactor(FE): Refactor table components for consistent styling and
cleanup
2026-02-19 11:14:39 +07:00
rstubryan c3a69bc66a refactor(FE): Add Icon component and update table styles 2026-02-19 10:55:07 +07:00
rstubryan 4c1f11d859 refactor(FE): Update table column headers from '#' to 'No' 2026-02-19 10:50:52 +07:00
rstubryan 350ff0fbbe refactor(FE): Simplify headerRowClassName formatting in tables 2026-02-19 10:46:03 +07:00
rstubryan 4c70ec7cab refactor(FE): Refactor table components to improve styling and structure 2026-02-19 10:43:37 +07:00
rstubryan 944db8dba7 refactor(FE): Refactor Card and SapronakClosingSkeleton components for
readability
2026-02-19 10:15:20 +07:00
rstubryan befc1c1217 refactor(FE): Refactor skeleton components to remove default columns 2026-02-19 10:14:28 +07:00
rstubryan 8fe19feaac feat(FE): Add skeleton components for closing pages 2026-02-19 10:03:25 +07:00
rstubryan 9c953ca382 refactor(FE): Fix incorrect import and component usage in
SapronakClosingTab
2026-02-19 09:37:54 +07:00
rstubryan c53430fa1f refactor(FE): Organize Sapronak tables into a dedicated folder 2026-02-19 09:35:42 +07:00
rstubryan 1fe722cb81 refactor(FE): Refactor code formatting for consistency and readability 2026-02-19 09:33:31 +07:00
rstubryan d9bd73d8c1 refactor(FE): Refactor sales data fetching and component structure 2026-02-19 09:32:33 +07:00
rstubryan 0235494d46 refactor(FE): Refactor HPP Expedition handling in ClosingDetailPage 2026-02-19 09:29:01 +07:00
rstubryan 32354e3c2d refactor(FE): Adjust padding on tab header wrapper in ClosingDetailTabs 2026-02-18 16:32:26 +07:00
rstubryan 14e1c59a69 refactor(FE): Refactor ClosingDetail component to use tab store 2026-02-18 16:28:56 +07:00
rstubryan 42cc0f2661 refactor(FE): Refactor component and file names for consistency 2026-02-18 16:15:27 +07:00
rstubryan 2f5d518e15 refactor(FE): Move table components to a shared "table" directory 2026-02-18 15:43:52 +07:00
rstubryan d085b18788 refactor(FE): Refactor folder structure for closing-related components 2026-02-18 15:40:06 +07:00
Rivaldi A N S d68bedf5ce Merge branch 'fix/refactor-project-flock-detail' into 'development'
[FIX/FE] Refactor Project Flock Detail UI

See merge request mbugroup/lti-web-client!326
2026-02-18 08:21:57 +00:00
rstubryan 2169c0ea62 refactor(FE): Update StatusBadge color and text in ChickinForm 2026-02-18 15:00:26 +07:00
rstubryan 02165df89c refactor(FE): Update status badge text to use English labels 2026-02-18 14:50:11 +07:00
rstubryan 15289951e6 refactor(FE): Remove unused CSS classes from table components 2026-02-18 14:42:41 +07:00
rstubryan 62674044e7 refactor(FE): Replace Badge with StatusBadge in ProjectFlockDetail 2026-02-18 14:33:28 +07:00
rstubryan e94967ea4c refactor(FE): Adjust grid column layout based on selectedKandang status 2026-02-18 14:28:55 +07:00
rstubryan ed576fc8eb feat(FE): Improve empty state handling and add "Unclose Flock"
functionality
2026-02-18 13:49:02 +07:00
rstubryan d4c6a05c0c refactor(FE): Update button behavior based on kandang status 2026-02-18 13:21:29 +07:00
rstubryan da27f4c581 refactor(FE): Refactor status badge logic in ProjectFlockClosingForm 2026-02-18 13:15:06 +07:00
rstubryan 9d6cc90162 refactor(FE): Update table columns and improve UI for Project Flock
pages
2026-02-18 11:52:30 +07:00
rstubryan 512ccddfc7 refactor(FE): Refactor ChickinForm and ProjectFlockClosingForm
components
2026-02-18 09:59:50 +07:00
Rivaldi A N S f5b16b68e9 Merge branch 'dev/hotfix/restu' into 'development'
[HOTFIX/FE] Refactor fetching logic useSelect on Depletion (Recording)

See merge request mbugroup/lti-web-client!325
2026-02-15 03:11:31 +00:00
rstubryan e8e4f7b877 refactor(FE): Simplify product filtering logic in RecordingForm 2026-02-15 08:47:07 +07:00
Rivaldi A N S b6edd8f10c Merge branch 'feat/refactor-report-module-ui' into 'development'
[FEAT/FE] Refactor UI on Report Module and Additional Adjustment (Project Flock, Product Stock, Marketing)

See merge request mbugroup/lti-web-client!324
2026-02-13 03:59:03 +00:00
rstubryan ec3a0367dd refactor(FE): Hide delete button if only one item remains in table 2026-02-13 10:37:12 +07:00
rstubryan e9da5210ad refactor(FE): Simplify type definition for ReportExpenseColumn 2026-02-13 10:22:13 +07:00
rstubryan 67f2a80f23 refactor(FE): Refactor expense report page to use tab-based layout 2026-02-13 10:19:09 +07:00
rstubryan ceb594a4cc refactor(FE): Rename and update paths for ProductionResult components 2026-02-13 09:55:22 +07:00
rstubryan d312da4c66 refactor(FE): Refactor dropdown and export button components 2026-02-13 09:53:33 +07:00
rstubryan 3a676723e4 refactor(FE): Refactor table class names in DailyMarketingTab 2026-02-13 09:42:04 +07:00
rstubryan 684f67593f refactor(FE): Refactor SelectInput styles for improved readability 2026-02-13 09:32:45 +07:00
rstubryan d5962f94a1 refactor(FE): Refactor production result filter to use OptionType 2026-02-13 09:29:37 +07:00
rstubryan 5c00893ea3 refactor(FE): Refactor production result components and improve UI 2026-02-13 09:24:42 +07:00
rstubryan 211622c7b0 refactor(FE): Format import statements in report-tab.store.ts 2026-02-12 16:19:40 +07:00
rstubryan dbb523c710 refactor(FE): Refactor tab store to use a unified ReportTabStore 2026-02-12 16:16:40 +07:00
rstubryan 6aae18df54 refactor(FE): Update import paths for finance and marketing tab stores 2026-02-12 15:49:26 +07:00
rstubryan cb171118ee refactor(FE): Restrict row selection to specific approval criteria 2026-02-12 15:32:11 +07:00
rstubryan 45ac8348fe feat(FE): Add "Stock Akhir" column to StockLogTable 2026-02-12 15:23:17 +07:00
rstubryan 5d92e6774e feat(FE): Add stock field to StockLog type 2026-02-12 15:14:00 +07:00
rstubryan 6595ff7a6e refactor(FE): Add function to export production results to Excel 2026-02-12 15:03:24 +07:00
rstubryan dc4e945a35 refactor(FE): Refactor table column definitions to remove unused row
parameter
2026-02-12 14:50:19 +07:00
rstubryan b154b478bc refactor(FE): Refactor production result components structure 2026-02-12 14:15:56 +07:00
rstubryan 510573e66f refactor(FE): Add Excel export functionality for daily marketing report 2026-02-12 13:55:31 +07:00
rstubryan dbcf469123 refactor(FE): Remove DailyMarketingsTable component and refactor related
files
2026-02-12 13:44:22 +07:00
rstubryan 325fb373a8 refactor(FE): Rename components for clarity 2026-02-12 13:19:16 +07:00
rstubryan 4b6a8b2773 refactor(FE): Refactor file names for consistency in marketing report
components
2026-02-12 11:45:50 +07:00
rstubryan 5e4619fac7 feat(FE): Add DailyMarketingReportFilter and
DailyMarketingReportSkeleton components
2026-02-12 11:38:46 +07:00
rstubryan 43d26b4833 refactor(FE): Refactor marketing report components and add HPP filter 2026-02-12 11:16:26 +07:00
rstubryan 6d2855d117 refactor(FE): Add zustand store for marketing tab actions 2026-02-12 11:15:42 +07:00
Adnan Zahir 25fbf95062 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!323
2026-02-12 11:06:11 +07:00
rstubryan ee53ea61cc refactor(FE): Fix missing dependency in useCallback hooks 2026-02-12 10:57:22 +07:00
rstubryan 322b519def refactor(FE): Refactor filter schema and form handling for
PurchasesPerSupplier
2026-02-12 10:55:40 +07:00
rstubryan e23b53d797 refactor(FE): Refactor CustomerPaymentTab to use StatusBadge component 2026-02-12 10:46:06 +07:00
rstubryan fd78ca6ac1 refactor(FE): Refactor CustomerPaymentTab to use Formik for filter
management
2026-02-12 10:28:36 +07:00
rstubryan 28dabcbeb6 refactor(FE): Refactor filter parameter keys to singular form 2026-02-12 09:52:21 +07:00
rstubryan 62dd1de150 refactor(FE): Reset form values on filter modal open and submit 2026-02-12 09:45:18 +07:00
rstubryan 166e95930b refactor(FE): Remove unused imports and cleanup comments 2026-02-12 09:35:24 +07:00
rstubryan 52d58d0921 refactor(FE): Refactor PurchasesPerSupplierTab to use Formik for filters 2026-02-11 16:44:10 +07:00
rstubryan 14d0dc590f refactor(FE): Make dropdown filters clearable in PurchasesPerSupplierTab 2026-02-11 16:17:15 +07:00
rstubryan ed781da372 refactor(FE): Change Tabs variant from 'lifted' to 'boxed' 2026-02-11 16:12:14 +07:00
rstubryan 4e5745d237 refactor(FE): Add tab state management and skeleton for
PurchasesPerSupplierTab
2026-02-11 15:53:32 +07:00
Rivaldi A N S b03ef4923e Merge branch 'hotfix/purchase-and-project-flock-form' into 'development'
[HOTFIX/FE] Refactor Purchase (Staff & Receipt/Receive) and Project Flock Form (Modal)

See merge request mbugroup/lti-web-client!322
2026-02-11 07:29:22 +00:00
Rivaldi A N S d7486e8b8a Merge branch 'fix/refactor-native-pdf-renderer' into 'development'
[FIX/FE] Refactor Native PDF Renderer to Component Based

See merge request mbugroup/lti-web-client!321
2026-02-11 07:28:43 +00:00
rstubryan 498602a2c9 refactor(FE): Refactor input components to use consistent color
variables
2026-02-11 14:19:52 +07:00
rstubryan 1b4d373fea refactor(FE): Improve disabled and read-only states for input components 2026-02-11 13:54:48 +07:00
rstubryan 4215b0ea7d feat(FE): Add success modal and state management for ProjectFlock 2026-02-11 13:44:00 +07:00
rstubryan c3dee6b292 feat(FE): Add Zustand store for ProjectFlock management 2026-02-11 13:42:56 +07:00
rstubryan 3834982fca feat(FE): Add "has_chickin" property to disable quantity editing 2026-02-11 11:40:29 +07:00
rstubryan 539de03a5b refactor(FE): Update section width to use full width 2026-02-11 10:41:57 +07:00
rstubryan 0f1d2ce477 refactor(FE): Refactor PDF table components to simplify imports 2026-02-11 10:39:37 +07:00
rstubryan 70b63f7773 refactor(FE): Refactor PdfTable components to support generic data types 2026-02-11 10:38:51 +07:00
rstubryan 02d13efc25 refactor(FE): Refactor table column headers for clarity and consistency 2026-02-11 09:28:24 +07:00
rstubryan 1227b7639f refactor(FE): Refactor ProductionResultReportPDF to use reusable PDF
components
2026-02-10 16:52:52 +07:00
rstubryan 5593463eab refactor(FE): Refactor marketing report components into a dedicated
folder
2026-02-10 16:20:19 +07:00
rstubryan be7b2a0f93 refactor(FE): Refactor DailyMarketingReportPDF component for cleaner
structure
2026-02-10 16:06:11 +07:00
rstubryan 4c6ac6e8e1 refactor(FE): Refactor PdfStatusBadge to use a single style prop 2026-02-10 14:04:44 +07:00
rstubryan 5cc51c52d9 feat(FE): Add PdfPageNumber component for rendering page numbers 2026-02-10 14:02:53 +07:00
rstubryan 59eb781a22 refactor(FE): Refactor PDF components to support custom styles 2026-02-10 14:00:28 +07:00
rstubryan 2af83bed8a refactor(FE): Refactor getDetailData to remove unused parameters 2026-02-10 13:37:34 +07:00
rstubryan 4775c1e115 refactor(FE): Refactor HppPerKandang PDF generation logic and UI
renderer
2026-02-10 12:01:51 +07:00
rstubryan d0dea834c1 refactor(FE): Refactor HppPerKandang export logic to use ExcelJS 2026-02-10 11:54:15 +07:00
rstubryan def894e5f4 refactor(FE): Refactor PDF generation for purchases per supplier 2026-02-10 11:43:23 +07:00
rstubryan 4f9401ed34 refactor(FE): Refactor export logic for PurchasesPerSupplier report 2026-02-10 11:36:27 +07:00
rstubryan 80763acc53 refactor(FE): Add utility for PDF badge styles and integrate into
reports
2026-02-10 11:27:43 +07:00
Rivaldi A N S 5fb065de3e Merge branch 'feat/reusable-pdf-component' into 'development'
[FEAT/FE] Reusable PDF Component and Hotfix Penjualan

See merge request mbugroup/lti-web-client!320
2026-02-10 02:09:59 +00:00
rstubryan d6b9161500 Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into feat/reusable-pdf-component 2026-02-10 09:01:15 +07:00
rstubryan bcc2070ed2 refactor(FE): Refactor DebtSupplierExportPDF to use reusable PDF
components
2026-02-09 21:50:35 +07:00
rstubryan e4e6e563c9 refactor(FE): Remove unused helper function formatTitleCaseGeneral 2026-02-09 17:07:00 +07:00
rstubryan efec9b6265 feat(FE): Add formatTitleCaseGeneral helper and update usage 2026-02-09 17:05:35 +07:00
rstubryan 4cf2f77265 fix(FE): Fix marketing type value logic in SalesOrderFormModal 2026-02-09 17:01:28 +07:00
rstubryan c86f0379b5 refactor(FE): Refactor CustomerPaymentExportPDF to use reusable PDF
components
2026-02-09 16:01:12 +07:00
rstubryan 606380460e feat(FE): Add PDF helper components for badges and typography 2026-02-09 15:58:28 +07:00
Rivaldi A N S a3bcabe5c2 Merge branch 'fix/remove-fcr-related' into 'development'
[FIX/FE] Remove FCR Related

See merge request mbugroup/lti-web-client!319
2026-02-09 08:58:26 +00:00
rstubryan 89ffad398f refactor(FE): Remove FCR-related links and permissions 2026-02-09 14:32:27 +07:00
rstubryan 35986aab56 refactor(FE): Remove FCR-related components and types 2026-02-09 14:28:02 +07:00
Rivaldi A N S 4717330bc8 Merge branch 'fix/adjustment-penjualan' into 'development'
[FIX/FE] Adjustment Penjualan UI and Data Fetch (2/2)

See merge request mbugroup/lti-web-client!318
2026-02-09 07:16:56 +00:00
rstubryan 291eee3bce refactor(FE): Add week field handling for marketing and sales order
forms
2026-02-09 14:06:43 +07:00
rstubryan e6a572ac17 refactor(FE): Add API call for updating delivery order on approval step
3
2026-02-09 13:31:03 +07:00
rstubryan bd5b614bf8 refactor(FE): Fix modal text logic for add_delivery action 2026-02-09 12:01:13 +07:00
rstubryan ba0753428d refactor(FE): Fix form reset and selection handling in
SalesOrderFormModal
2026-02-09 11:49:08 +07:00
rstubryan 862cf38f92 refactor(FE): Refactor DeliveryOrderProductTable to improve readability 2026-02-09 11:41:08 +07:00
rstubryan 1dc6ffca5c refactor(FE): Handle modal action to set step and selected delivery
product
2026-02-09 11:36:41 +07:00
rstubryan b7fd5d3569 refactor(FE): Add approval modal and handler for marketing approval 2026-02-09 11:25:19 +07:00
rstubryan 911136981a refactor(FE): Add clickable "Belum diisi" text for delivery forms 2026-02-09 11:23:49 +07:00
rstubryan 6cbe14b36e refactor(FE): Refactor SalesOrderProductTable to use renderTableContent
helper
2026-02-09 11:00:29 +07:00
rstubryan 80c79cc14b refactor(FE): Refactor DeliveryOrderProductTable to optimize conditional
rendering
2026-02-09 10:46:32 +07:00
rstubryan cb498b01d9 chore(FE): Refactor tables to remove unused props and imports 2026-02-09 10:26:23 +07:00
Rivaldi A N S cd95b1f8ff Merge branch 'fix/adjustment-penjualan' into 'development'
[FIX/FE] Adjustment Penjualan UI and Data Fetch

See merge request mbugroup/lti-web-client!317
2026-02-09 03:04:46 +00:00
rstubryan 60ace68dae refactor(FE): Remove FCR-related fields and functionality 2026-02-09 08:59:51 +07:00
rstubryan a8dce9da46 refactor(FE): Refactor table actions to be part of the "Value" column 2026-02-07 10:04:15 +07:00
rstubryan b85e47f601 refactor(FE): Add top border to table rows in product tables 2026-02-07 10:00:02 +07:00
rstubryan fc4a0a58e2 refactor(FE): Refactor tables to use Card component 2026-02-07 09:58:00 +07:00
rstubryan 039dfd529e refactor(FE): Increase max height of order form modals to 50vh 2026-02-07 09:33:58 +07:00
rstubryan 3b42709577 refactor(FE): Refactor modal action handling in order form modals 2026-02-07 09:32:34 +07:00
rstubryan 3dee5c1828 refactor(FE): Add reference number column to ExpensesTable 2026-02-07 09:29:05 +07:00
rstubryan 5ac958231a refactor(FE): Remove unused invoice download functionality from
PurchaseTable
2026-02-07 09:10:39 +07:00
rstubryan 54a6e7e247 refactor(FE): Refactor RecordingForm to simplify egg product filtering 2026-02-07 09:00:04 +07:00
rstubryan 4e80c1a703 refactor(FE): Fix warehouse name display in DeliveryOrderProductTable 2026-02-06 14:46:13 +07:00
rstubryan 9ee5e95d0b refactor(FE): Remove unused conditional rendering for step 3 2026-02-06 14:33:13 +07:00
rstubryan 3bacc59dc6 refactor(FE): Add condition to hide buttons when step number is 3 2026-02-06 14:32:30 +07:00
rstubryan 4d23929924 refactor(FE): Update approval logic and conditional rendering in
delivery forms
2026-02-06 14:20:52 +07:00
rstubryan c9a5a91970 refactor(FE): Update order number display logic in
DeliveryOrderFormModal
2026-02-06 14:20:10 +07:00
rstubryan 08d1447d11 refactor(FE): Update button label based on approval step number 2026-02-06 13:41:17 +07:00
rstubryan 304be4f432 feat(FE): Add support for displaying DO number in MarketingTable 2026-02-06 13:29:38 +07:00
rstubryan 5e9ce70320 feat(FE): Add "Ditolak" option to statusOptions in MarketingFilter 2026-02-06 13:21:33 +07:00
rstubryan 42088e51a8 refactor(FE): Refactor marketing filter to use unique customer options 2026-02-06 13:13:57 +07:00
rstubryan 9dc8f05534 feat: adjust penjualan calculation and delivery order logic 2026-02-06 10:55:38 +07:00
Adnan Zahir cc86151631 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!315
2026-02-06 10:39:39 +07:00
Rivaldi A N S e16fa9a167 Merge branch 'fix/project-flock' into 'development'
[FIX/FE] Project Flock

See merge request mbugroup/lti-web-client!314
2026-02-06 02:58:02 +00:00
ValdiANS 869110ad2e Merge branch 'development' into fix/project-flock 2026-02-06 09:51:20 +07:00
ValdiANS d415bbba82 feat: add getApprovalLineHistory method 2026-02-06 09:47:11 +07:00
ValdiANS 1ecca83339 feat: add getApprovalLineHistory method 2026-02-06 09:47:05 +07:00
ValdiANS a6c827bb40 feat: add PROJECT_FLOCK_KANDANGS in APPROVAL_WORKFLOWS 2026-02-06 09:46:51 +07:00
ValdiANS 968d9e1f2a fix: adjust ProjectFlockForm styling and create ProjectFlockFormConfirmationTable component 2026-02-06 09:46:37 +07:00
ValdiANS 7b9ba48204 fix: adjust ProjectFlockDetail styling and content 2026-02-06 09:46:00 +07:00
ValdiANS 6e2e9da1be chore: adjust ProjectFlockClosingForm styling 2026-02-06 09:45:38 +07:00
ValdiANS 980a5674e2 feat: create ProjectFlockConfirmationModal component 2026-02-06 09:45:07 +07:00
ValdiANS f5b9c52e71 chore: set chickin form scrollable 2026-02-06 09:44:56 +07:00
ValdiANS ade8fefe0d feat: auto scroll to AlertErrorList if AlertErrorList is showing 2026-02-06 09:44:33 +07:00
ValdiANS 6b54b49443 chore: adjust divider styling 2026-02-06 09:44:06 +07:00
ValdiANS 8fb16903f8 feat: add onClick prop and set text type to react node 2026-02-06 09:43:56 +07:00
ValdiANS f0637e2ce9 chore: add title prop 2026-02-06 09:43:34 +07:00
ValdiANS f6cf4a29ad feat: add ref prop to Alert 2026-02-06 09:43:27 +07:00
ValdiANS 66f017549c chore: use modal for project flock layout 2026-02-06 09:43:15 +07:00
ValdiANS db7219e261 chore: add shadow-bg class name 2026-02-06 09:42:45 +07:00
Rivaldi A N S ac6c77bb92 Merge branch 'dev/hotfix/restu' into 'development'
[HOTFIX/FE] Adjustment Param State on Customer Payment Tab

See merge request mbugroup/lti-web-client!313
2026-02-05 08:54:05 +00:00
rstubryan d5eeadc9a7 refactor(FE): Remove handleBlurField call on week change 2026-02-05 15:47:05 +07:00
rstubryan 70a9fa15ec refactor(FE): Switch week input to SelectInputRadio 2026-02-05 15:43:03 +07:00
rstubryan 4fd4374e64 refactor(FE): Validate date range and show toast on error 2026-02-05 15:04:12 +07:00
rstubryan b4353cf834 refactor(FE): Make filter type nullable and use applied filters 2026-02-05 14:55:27 +07:00
rstubryan eb95afe9a0 refactor(FE): Separate applied and modal filter state 2026-02-05 14:39:33 +07:00
rstubryan 92886fe5e2 refactor(FE): Consolidate date filters into trans_date 2026-02-05 14:28:14 +07:00
rstubryan fb1b310d1d refactor(FE): Replace SelectInput with SelectInputRadio 2026-02-05 14:13:13 +07:00
rstubryan 3b221795ba refactor(FE): Add filter_by option to customer payment reports 2026-02-05 14:08:05 +07:00
rstubryan d41600d8e2 refactor(FE): Replace week SelectInputRadio with NumberInput 2026-02-05 13:48:07 +07:00
Rivaldi A N S 856674de75 Merge branch 'dev/hotfix/restu' into 'development'
[HOTFIX/FE] Fix Meta Page on Penjualan Table Fetching

See merge request mbugroup/lti-web-client!312
2026-02-05 06:00:35 +00:00
rstubryan 1af2b72bea refactor(FE): Prevent badge text wrapping 2026-02-05 12:06:04 +07:00
rstubryan e66f30e703 refactor(FE): Use API metadata for table pagination 2026-02-05 12:04:08 +07:00
Rivaldi A N S ca32af592f Merge branch 'fix/adjustment-param-state-keuangan' into 'development'
[FIX/FE] Adjustment Param State on Fetch Keuangan

See merge request mbugroup/lti-web-client!311
2026-02-05 04:23:21 +00:00
rstubryan 372b439ff0 refactor(FE): Validate date range and show persistent toast 2026-02-05 11:13:53 +07:00
rstubryan 4aa9d54b1e refactor(FE): Remove unused search params and yup import 2026-02-05 11:02:20 +07:00
rstubryan b45c7c8ea6 refactor(FE): Use Formik for finance table filters 2026-02-05 10:58:59 +07:00
rstubryan c164977bb9 refactor(FE): Keep select menus open for multi-selects 2026-02-05 10:50:56 +07:00
rstubryan 3153423f14 refactor(FE): Replace size-full with w-full in FinanceTable 2026-02-05 10:48:26 +07:00
rstubryan ac3fbedccd refactor(FE): Rename filter keys to plural forms 2026-02-05 10:46:37 +07:00
Adnan Zahir 755f3fa0bb Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!310
2026-02-05 10:32:18 +07:00
Rivaldi A N S 9004de06fa Merge branch 'fix/marketing' into 'development'
[FIX/FE] Fixing UX Sales Order & Delivery Order

See merge request mbugroup/lti-web-client!309
2026-02-05 03:06:19 +00:00
randy-ar 4d7bd5213e fix(FE): fixing delivery order ui 2026-02-05 09:58:14 +07:00
randy-ar 2f1c4e3c87 fix(FE): fixing issue form not reset after success submit 2026-02-05 06:05:01 +07:00
randy-ar 43dcbf73ee feat(FE): adding 4 input scenario marketing type 2026-02-05 05:38:02 +07:00
randy-ar cb22fd1037 feat(FE): calculation penjualan telur + peti 2026-02-05 04:41:46 +07:00
randy-ar dfd86a04e0 feat(FE): scenario egg sale with type conversion qty 2026-02-05 02:39:10 +07:00
randy-ar 09cd6395e6 fix(FE): skeleton for input skenario sales order 2026-02-04 15:38:03 +07:00
Rivaldi A N S a8c9b697e3 Merge branch 'fix/purchase-adjustment-data' into 'development'
[FIX/FE] Refactor Purchase Adjustment Data Based on Latest BE API

See merge request mbugroup/lti-web-client!308
2026-02-04 07:24:16 +00:00
ValdiANS c019162390 Merge branch 'development' into fix/project-flock 2026-02-04 10:36:25 +07:00
ValdiANS 1ee92f1064 Merge branch 'development' into fix/project-flock 2026-02-03 12:07:13 +07:00
ValdiANS dc5bd6b329 chore: adjust ProjectFlockKandangTable styling 2026-02-03 09:40:34 +07:00
ValdiANS 68f3c95b81 chore: adjust ProjectFlockForm styling 2026-02-03 09:40:16 +07:00
ValdiANS d826746f29 chore: adjust DrawerHeader styling 2026-02-03 09:39:58 +07:00
ValdiANS 39b18f7efc refactor: use Modal instead of Drawer 2026-02-03 09:17:17 +07:00
ValdiANS c19a7cba68 chore: lint 2026-02-02 11:15:38 +07:00
ValdiANS ec7427b948 Merge branch 'development' into fix/project-flock 2026-02-02 11:14:54 +07:00
ValdiANS 448fb51c81 Merge branch 'development' into fix/project-flock 2026-02-02 08:50:53 +07:00
Adnan Zahir 128b765045 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!294
2026-01-30 17:05:12 +07:00
Adnan Zahir 1aba297920 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!291
2026-01-30 15:54:42 +07:00
Adnan Zahir 2aef6522bb Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!286
2026-01-30 13:32:04 +07:00
Adnan Zahir 3bab96c325 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!283
2026-01-30 11:40:25 +07:00
ValdiANS 4e801bf744 Merge branch 'development' into fix/project-flock 2026-01-30 10:10:01 +07:00
ValdiANS c5269c4fc5 fix: adjust ProjectFlockTable UI 2026-01-29 16:17:27 +07:00
ValdiANS 4e00ded843 chore: add new step in PROJECT_FLOCKS approval workflows 2026-01-29 15:56:31 +07:00
Adnan Zahir 344140e973 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!271
2026-01-28 13:26:27 +07:00
Adnan Zahir 3ce1299091 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!268
2026-01-28 10:07:39 +07:00
Adnan Zahir 5b134148a5 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!258
2026-01-27 09:13:31 +07:00
Adnan Zahir 04d01970aa Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!253
2026-01-24 14:19:24 +07:00
Adnan Zahir 9176373072 Merge branch 'development' into 'staging'
Development: Transfer To Laying Rework

See merge request mbugroup/lti-web-client!248
2026-01-24 12:59:23 +07:00
Adnan Zahir 7e64ec0f79 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!243
2026-01-24 11:16:16 +07:00
Adnan Zahir 9322d6298c Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!236
2026-01-22 16:14:12 +07:00
Adnan Zahir 89cfd31155 Merge branch 'development' into 'staging'
fix(FE): change nominal to absolute value, change form state initial balance,...

See merge request mbugroup/lti-web-client!228
2026-01-21 15:08:55 +07:00
Adnan Zahir 0eb4fa99a7 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!215
2026-01-20 11:46:20 +07:00
Adnan Zahir 2ef8b2dc9f Merge branch 'development' into 'staging'
Merge branch 'dev/hotfix/restu' into 'staging'

See merge request mbugroup/lti-web-client!203
2026-01-17 14:36:51 +07:00
Adnan Zahir aed1a1ed01 Merge branch 'development' into 'staging'
Hotfixes flock

See merge request mbugroup/lti-web-client!201
2026-01-17 11:29:55 +07:00
Adnan Zahir 2c9c2660c0 Merge branch 'development' into 'staging'
fix(FE): fix limit fetch data kandang

See merge request mbugroup/lti-web-client!198
2026-01-17 10:41:25 +07:00
Adnan Zahir b840f42ae0 Merge branch 'development' into 'staging'
refactor(FE): Improve vehicle number validation message and set

See merge request mbugroup/lti-web-client!196
2026-01-17 09:05:42 +07:00
Adnan Zahir 6bc86af32f Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!189
2026-01-15 16:21:40 +07:00
kris 1603ae62e0 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!182
2026-01-15 06:55:35 +00:00
M1 AIR bd4242c4fd chore: fix conflict gitlab yml 2026-01-13 15:38:14 +07:00
M1 AIR 56bde974ad chore: gitlab ci yml 2026 01 13 2026-01-13 15:36:56 +07:00
M1 AIR 38258e4311 Merge remote-tracking branch 'origin/development' into staging 2026-01-13 15:27:31 +07:00
kris 149e525ff4 Update .gitlab-ci.yml file 2026-01-10 02:15:18 +00:00
M1 AIR 8fb761f02c Merge remote-tracking branch 'origin/development' into staging 2026-01-09 15:53:47 +07:00
M1 AIR 3bc5a5b75e delete .gitlab 2026-01-09 13:16:42 +07:00
M1 AIR 79112e0da8 Penyesuaian flow repo 2026-01-09 10:52:56 +07:00
M1 AIR bf9eb91ea2 Merge remote-tracking branch 'origin/development' into staging 2026-01-06 19:03:21 +07:00
kris e8c8ffadfe Update .gitlab-ci.yml file 2026-01-03 11:01:19 +00:00
M1 AIR 2ae1c5b382 Merge development into staging (keep staging CI config) 2026-01-03 16:43:49 +07:00
kris 961f81411b Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-web-client!38
2025-12-18 10:30:13 +00:00
Mitra Berlian Unggas de439275e0 Merge branch 'development' into 'staging'
refactor(FE-114): streamline cost field validation messages and enhance layout...

See merge request mbugroup/lti-web-client!37
2025-10-28 08:44:08 +00:00
260 changed files with 17032 additions and 15472 deletions
+28 -15
View File
@@ -1,25 +1,38 @@
FROM node:20-alpine
RUN apk add --no-cache git bash build-base curl
# =========================
# Builder stage
# =========================
FROM golang:1.23-alpine AS builder
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Buat config agar Next tahu output: export
RUN echo "const config = { output: 'export', images: { unoptimized: true } }; export default config;" > next.config.mjs
# Build API binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o lti-api ./cmd/api
# Build project (Next.js 15 otomatis static export)
RUN NEXT_DISABLE_TURBOPACK=1 npx next build
# Build SEED binary (pastikan cmd/seed ada)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o lti-seed ./cmd/seed
# Copy static assets dan hasil build agar bisa diakses
RUN mkdir -p .next/server/app/_next && \
cp -r .next/static .next/server/app/_next/static && \
cp -r public/* .next/server/app/
# =========================
# Runtime stage
# =========================
FROM alpine:3.20
EXPOSE 3000
RUN apk add --no-cache ca-certificates tzdata curl bash postgresql-client \
&& adduser -D -H -u 10001 appuser
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
WORKDIR /app
COPY --from=builder /app/lti-api /app/lti-api
COPY --from=builder /app/lti-seed /app/lti-seed
USER appuser
EXPOSE 8081
CMD ["/app/lti-api"]
-39
View File
@@ -1,39 +0,0 @@
version: '3.9'
services:
dev-web-lti:
container_name: dev-web-lti
build:
context: .
dockerfile: Dockerfile
ports:
- '3002:3000'
env_file:
- .env
environment:
NODE_ENV: production
APP_ENV: production
networks:
- dev-lti-network
restart: always
deploy:
resources:
limits:
cpus: '3.0'
memory: 3G
reservations:
cpus: '1.0'
memory: 512M
extra_hosts:
- 'host.docker.internal:host-gateway'
# Optional: aktifkan healthcheck jika punya endpoint
# healthcheck:
# test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/healthz || exit 1"]
# interval: 10s
# timeout: 3s
# retries: 10
# start_period: 15s
networks:
dev-lti-network:
external: true
+2 -41
View File
@@ -3,11 +3,10 @@
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import ClosingDetail from '@/components/pages/closing/ClosingDetail';
import ClosingDetail from '@/components/pages/closing/ClosingDetailTabs';
import { ClosingApi } from '@/services/api/closing';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { FlockApi } from '@/services/api/master-data';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { ProjectFlockKandangApi } from '@/services/api/production';
@@ -34,33 +33,6 @@ const ClosingDetailPage = () => {
() => ProjectFlockKandangApi.getSingle(Number(kandangId))
);
const { data: salesData, isLoading: isLoadingSales } = useSWR(
kandangId
? `sales-${closingId}-${kandangId}`
: closingId
? `sales-${closingId}`
: null,
() =>
kandangId
? ClosingApi.getPenjualanByKandang(Number(closingId), Number(kandangId))
: ClosingApi.getPenjualan(Number(closingId))
);
const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR(
kandangId
? `hpp-ekspedisi-${closingId}-${kandangId}`
: closingId
? `hpp-ekspedisi-${closingId}`
: null,
() =>
kandangId
? ClosingApi.getHppEkspedisiByKandang(
Number(closingId),
Number(kandangId)
)
: ClosingApi.getHppEkspedisi(Number(closingId))
);
if (!closingId) {
router.back();
@@ -76,12 +48,7 @@ const ClosingDetailPage = () => {
return;
}
const isLoading =
isLoadingClosing ||
isLoadingSales ||
isLoadingHppEkspedisi ||
isLoadingProject ||
isLoadingKandang;
const isLoading = isLoadingClosing || isLoadingProject || isLoadingKandang;
return (
<div className='w-full p-4 flex flex-row justify-center'>
@@ -91,12 +58,6 @@ const ClosingDetailPage = () => {
<ClosingDetail
id={Number(closingId)}
initialValue={closing.data}
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
hppExpeditionData={
isResponseSuccess(hppEkspedisiData)
? hppEkspedisiData.data
: undefined
}
projectData={
isResponseSuccess(projectData) ? projectData.data : undefined
}
+1 -1
View File
@@ -2,7 +2,7 @@ import ClosingsTable from '@/components/pages/closing/ClosingsTable';
const Closing = () => {
return (
<section className='w-full p-4'>
<section className='w-full p-3'>
<ClosingsTable />
</section>
);
-1
View File
@@ -5,7 +5,6 @@ import useSWR from 'swr';
import { FinanceApi } from '@/services/api/finance';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
const EditFinanceTransactionPage = () => {
const router = useRouter();
+1 -1
View File
@@ -4,7 +4,7 @@ import FinanceDetail from '@/components/pages/finance/FinanceDetail';
import useSWR from 'swr';
import { useRouter, useSearchParams } from 'next/navigation';
import { FinanceApi } from '@/services/api/finance';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { isResponseSuccess } from '@/lib/api-helper';
const FinanceDetailPage = () => {
const router = useRouter();
+2
View File
@@ -68,6 +68,8 @@
--shadow-button-soft:
0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200);
--shadow-bg: 0px -2px 4px 0px #00000014;
}
html {
@@ -1,11 +0,0 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -1,54 +0,0 @@
'use client';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const EditMarketingDelivery = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('marketingId');
const {
data: marketing,
isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(`get-so-${soId}`, () =>
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
);
if (!soId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoading && (!marketing || isResponseError(marketing))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<MarketingForm
formType='add_deliver'
initialValues={marketing.data}
afterSubmit={() => {
refreshMarketing();
}}
/>
)}
</div>
);
};
export default EditMarketingDelivery;
@@ -1,11 +0,0 @@
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
const AddSalesOrder = () => {
return (
<div className='size-full p-4'>
<MarketingForm formType='add' />
</div>
);
};
export default AddSalesOrder;
@@ -1,11 +0,0 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -1,62 +0,0 @@
'use client';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const EditMarketingDelivery = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('marketingId');
const {
data: marketing,
isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(`get-so-${soId}`, () =>
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
);
if (!soId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoading && (!marketing || isResponseError(marketing))) {
router.replace('/404');
return;
}
if (
isResponseSuccess(marketing) &&
marketing.data.latest_approval.step_number != 3
) {
toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!');
router.back();
}
return (
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<MarketingForm
formType='edit_deliver'
initialValues={marketing.data}
afterSubmit={() => {
refreshMarketing();
}}
/>
)}
</div>
);
};
export default EditMarketingDelivery;
-11
View File
@@ -1,11 +0,0 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
-49
View File
@@ -1,49 +0,0 @@
'use client';
import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const DetailMarketing = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('marketingId');
const {
data: marketing,
isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
if (!soId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoading && (!marketing || isResponseError(marketing))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<MarketingDetail
initialValues={marketing.data}
refresh={refreshMarketing}
/>
)}
</div>
);
};
export default DetailMarketing;
@@ -1,11 +0,0 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -1,52 +0,0 @@
'use client';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const EditSalesOrder = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('marketingId');
const {
data: marketing,
isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(`get-so-${soId}`, () =>
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
);
if (!soId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoading && (!marketing || isResponseError(marketing))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<MarketingForm
formType='edit'
initialValues={marketing.data}
afterSubmit={() => {
refreshMarketing();
}}
/>
)}
</div>
);
};
export default EditSalesOrder;
-11
View File
@@ -1,11 +0,0 @@
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;
@@ -1,52 +0,0 @@
'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
@@ -1,11 +0,0 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
-52
View File
@@ -1,52 +0,0 @@
'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
@@ -1,11 +0,0 @@
import FcrsTable from '@/components/pages/master-data/fcr/FcrsTable';
const Fcr = () => {
return (
<section className='w-full p-4'>
<FcrsTable />
</section>
);
};
export default Fcr;
+1 -2
View File
@@ -3,10 +3,9 @@
import { useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { useAuth } from '@/services/hooks/useAuth';
import { redirectToSSO } from '@/lib/auth-helper';
export default function Home() {
const { user, isLoadingUser } = useAuth();
const { isLoadingUser } = useAuth();
const router = useRouter();
const pathname = usePathname();
@@ -1,8 +1,8 @@
'use client';
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import React, { useImperativeHandle } from 'react';
import toast from 'react-hot-toast';
import React from 'react';
// import React, { useImperativeHandle } from 'react';
const AddProjectFlock = () => {
// useImperativeHandle(ref, () => ({
@@ -12,11 +12,10 @@ const ProjectFlockEdit = () => {
const projectFlockId = searchParams.get('projectFlockId');
const {
data: projectFlock,
isLoading: isLoadingProjectFlock,
mutate: refreshProjectFlocks,
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
projectFlockId,
(id: number) => ProjectFlockApi.getSingle(id)
);
if (!projectFlockId) {
router.back();
@@ -1,7 +1,6 @@
'use client';
import ProjectFlockDetail from '@/components/pages/production/project-flock/detail/ProjectFlockDetail';
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useRouter, useSearchParams } from 'next/navigation';
@@ -13,11 +12,10 @@ const ProjectFlockDetailPage = () => {
const projectFlockId = searchParams.get('projectFlockId');
const {
data: projectFlock,
isLoading: isLoadingProjectFlock,
mutate: refreshProjectFlock,
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
projectFlockId,
(id: number) => ProjectFlockApi.getSingle(id)
);
if (!projectFlockId) {
router.back();
+25 -13
View File
@@ -1,10 +1,10 @@
'use client';
import { usePathname, useRouter } from 'next/navigation';
import Drawer from '@/components/Drawer';
import React, { ReactNode } from 'react';
import React, { ReactNode, useEffect } from 'react';
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
import { useUiStore } from '@/stores/ui/ui.store';
import Modal, { useModal } from '@/components/Modal';
export default function ProjectFlockLayout({
children,
@@ -23,9 +23,12 @@ export default function ProjectFlockLayout({
const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing;
const formModal = useModal();
const handleBackdropClick = () => {
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
if (isValid) {
formModal.closeModal();
unsub(); // berhenti listen
router.push('/production/project-flock');
}
@@ -34,6 +37,14 @@ export default function ProjectFlockLayout({
toggleValidate();
};
useEffect(() => {
if (isOpen && !formModal.open) {
formModal.openModal();
} else {
formModal.closeModal();
}
}, [isOpen]);
return (
<>
{/* List page always rendered */}
@@ -43,18 +54,19 @@ export default function ProjectFlockLayout({
/>
</div>
{/* Render Drawer only on /add */}
<Drawer
open={isOpen}
setOpen={(v) => {
if (!v) router.push('/production/project-flock');
}}
closeOnBackdropClick={isDetail ? true : false}
{/* Render Modal only on /add */}
<Modal
ref={formModal.ref}
position='end'
onBackdropClick={handleBackdropClick}
variant='right'
zIndex='99999'
sidebarContent={isOpen && <div className=''>{children}</div>}
/>
className={{
modalBox: 'w-full sm:w-fit p-3 rounded-xl bg-transparent shadow-none',
}}
>
<div className='w-full sm:w-[446px] h-full flex flex-col sm:flex-row items-stretch bg-base-100 rounded-xl overflow-hidden'>
{isOpen && children}
</div>
</Modal>
</>
);
}
+2 -6
View File
@@ -1,13 +1,9 @@
'use client';
import ReportExpenseTable from '@/components/pages/report/expense/ReportExpenseTable';
import ReportExpenseTabs from '@/components/pages/report/expense/ReportExpenseTabs';
const ReportExpense = () => {
return (
<div className='w-full p-4'>
<ReportExpenseTable />
</div>
);
return <ReportExpenseTabs />;
};
export default ReportExpense;
+2 -6
View File
@@ -1,11 +1,7 @@
import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
import MarketingReportContent from '@/components/pages/report/marketing/MarketingTabs';
const MarketingReportPage = () => {
return (
<section className='w-full p-4'>
<MarketingReportContent />
</section>
);
return <MarketingReportContent />;
};
export default MarketingReportPage;
+3 -3
View File
@@ -1,9 +1,9 @@
import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent';
import ProductionResultTabs from '@/components/pages/report/production-result/ProductionResultTabs';
const ProductionResultReportPage = () => {
return (
<section className='w-full max-w-7xl pb-16'>
<ProductionResultContent />
<section className='w-full max-w-full'>
<ProductionResultTabs />
</section>
);
};
+8 -3
View File
@@ -1,15 +1,16 @@
import { ReactNode } from 'react';
import { ReactNode, Ref } from 'react';
import { cn } from '@/lib/helper';
interface AlertProps {
ref?: Ref<HTMLDivElement> | undefined;
variant?: 'outline' | 'dash' | 'soft';
color?: 'info' | 'success' | 'warning' | 'error';
children?: ReactNode;
className?: string;
}
const Alert = ({ children, variant, color, className }: AlertProps) => {
const Alert = ({ children, ref, variant, color, className }: AlertProps) => {
const alertBaseClassName = cn('alert', {
'alert-soft': variant === 'soft',
'alert-outline': variant === 'outline',
@@ -21,7 +22,11 @@ const Alert = ({ children, variant, color, className }: AlertProps) => {
'alert-error': color === 'error',
});
return <div className={cn(alertBaseClassName, className)}>{children}</div>;
return (
<div ref={ref} className={cn(alertBaseClassName, className)}>
{children}
</div>
);
};
export default Alert;
-2
View File
@@ -1,6 +1,5 @@
'use client';
import { useCallback } from 'react';
import { usePathname } from 'next/navigation';
import Image from 'next/image';
@@ -13,7 +12,6 @@ import PermissionNotFound from '@/components/helper/PermissionNotFound';
import { useUiStore } from '@/stores/ui/ui.store';
import { MAIN_DRAWER_LINKS } from '@/config/constant';
import { isPathActive } from '@/lib/helper';
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
import { useAuth } from '@/services/hooks/useAuth';
+16 -6
View File
@@ -85,8 +85,8 @@ const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({
}));
const emptyContentDefaultValue = (
<div className='w-full p-5 text-center'>
<span className='text-lg opacity-50'>
<div className='w-full text-center py-4'>
<span className='text-sm opacity-50'>
Tidak ada data yang dapat ditampilkan...
</span>
</div>
@@ -452,6 +452,20 @@ const Table = <TData extends object>({
</Fragment>
);
})}
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
!isLoading && (
<tr>
<td
colSpan={
table.getAllLeafColumns().length + (withCheckbox ? 1 : 0)
}
className='p-0'
>
{emptyContent}
</td>
</tr>
)}
</tbody>
<tfoot
className={cn(
@@ -489,10 +503,6 @@ const Table = <TData extends object>({
</table>
</div>
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
!isLoading &&
emptyContent}
{data.length > 0 &&
table.getRowModel().rows.length > 0 &&
!isLoading &&
+1 -1
View File
@@ -1,4 +1,4 @@
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react';
import { HTMLAttributes, ReactNode, useState } from 'react';
import { cn } from '@/lib/helper';
export interface TabItem {
+3 -1
View File
@@ -9,6 +9,7 @@ import Button from '@/components/Button';
import { cn, formatDate } from '@/lib/helper';
interface ApprovalStepsV2Props {
title?: string;
approvals?: BaseApproval[];
steps: {
step_number: number;
@@ -23,6 +24,7 @@ interface ApprovalStepsV2Props {
}
const ApprovalStepsV2 = ({
title = 'Progress Details',
approvals,
steps,
maxVisibleSteps = 2,
@@ -99,7 +101,7 @@ const ApprovalStepsV2 = ({
)}
>
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
Progress Details
{title}
</h4>
<div
+7 -1
View File
@@ -1,24 +1,30 @@
import { ReactNode } from 'react';
import Badge from '@/components/Badge';
import { cn } from '@/lib/helper';
import { Color } from '@/types/theme';
interface StatusBadgeProps {
color: Color;
text: string;
text: ReactNode;
className?: {
badge?: string;
status?: string;
};
onClick?: () => void;
}
const StatusBadge = ({
color = 'neutral',
text,
className,
onClick,
}: StatusBadgeProps) => {
return (
<Badge
variant='soft'
onClick={onClick}
className={{
badge: cn(
'px-2 py-1 w-full flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content',
@@ -27,7 +27,7 @@ export interface DrawerHeaderProps {
const DrawerHeader = ({
leftIcon = 'mdi:close',
leftIconSize = 24,
leftIconSize = 20,
leftIconHref,
leftIconOnClick,
leftIconClassName,
@@ -43,7 +43,7 @@ const DrawerHeader = ({
icon={leftIcon}
width={leftIconSize}
height={leftIconSize}
className={cn('cursor-pointer', leftIconClassName)}
className={cn('cursor-pointer text-base-content ', leftIconClassName)}
/>
);
@@ -73,7 +73,7 @@ const DrawerHeader = ({
return (
<div
className={cn(
'flex flex-row justify-between items-center px-4 pt-4 pb-4 border-b border-base-content/10',
'flex flex-row justify-between items-center p-4 border-b border-base-content/10',
className
)}
>
@@ -82,7 +82,7 @@ const DrawerHeader = ({
{renderLeftIcon()}
{showDivider && subtitle && (
<div className='divider divider-horizontal p-0 m-0'></div>
<div className='w-px h-full border-none bg-base-content/10' />
)}
{subtitle && (
+16 -1
View File
@@ -1,8 +1,10 @@
'use client';
import Alert from '@/components/Alert';
import Button from '@/components/Button';
import { cn } from '@/lib/helper';
import { Icon } from '@iconify/react';
import { useState } from 'react';
import { useEffect, useRef } from 'react';
/**
* Alert Unique Error List
@@ -29,10 +31,22 @@ const AlertErrorList = ({
onClose: () => void;
title?: string;
}) => {
const alertRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (formErrorList.length > 0) {
alertRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [formErrorList.length]);
if (formErrorList.length === 0) return null;
return (
<Alert
ref={alertRef}
color='error'
className={cn(
'w-full flex flex-col gap-2 px-3 rounded-lg',
@@ -57,6 +71,7 @@ const AlertErrorList = ({
</span>
</div>
<Button
type='button'
onClick={onClose}
variant='link'
className={cn('ml-auto p-0 w-fit text-white', className?.button)}
@@ -0,0 +1,27 @@
import { Text, View, StyleSheet } from '@react-pdf/renderer';
import type { Style } from '@react-pdf/types';
type PdfParamBadgeProps = {
children: React.ReactNode;
style?: Style;
};
const styles = StyleSheet.create({
parameterBadge: {
backgroundColor: '#F5F5F5',
color: '#333333',
padding: 4,
borderRadius: 4,
fontSize: 8,
marginRight: 8,
marginBottom: 4,
},
});
export const PdfParamBadge = ({ children, style }: PdfParamBadgeProps) => {
return (
<View style={[styles.parameterBadge, ...(style ? [style] : [])]}>
<Text>{children}</Text>
</View>
);
};
@@ -0,0 +1,54 @@
import { Text, View, StyleSheet } from '@react-pdf/renderer';
import type { Style } from '@react-pdf/types';
type PdfStatusBadgeProps = {
children: React.ReactNode;
style?: Style;
};
const styles = StyleSheet.create({
statusBadge: {
paddingVertical: 2,
paddingHorizontal: 4,
borderRadius: 12,
fontSize: 7,
fontWeight: 'bold',
borderWidth: 1,
borderStyle: 'solid',
backgroundColor: '#F5F5F5',
borderColor: '#E5E7EB',
},
statusBadgeText: {
fontSize: 7,
fontWeight: 'bold',
color: '#333333',
},
});
export const PdfStatusBadge = ({ children, style }: PdfStatusBadgeProps) => {
const styleRecord = style as Record<string, unknown>;
const color = styleRecord?.color as string | undefined;
const viewStyle = Object.entries(styleRecord || {}).reduce(
(acc, [key, value]) => {
if (key !== 'color') {
acc[key] = value;
}
return acc;
},
{} as Record<string, unknown>
);
return (
<View
style={[
styles.statusBadge,
...(Object.keys(viewStyle).length > 0 ? [viewStyle as Style] : []),
]}
>
<Text style={[styles.statusBadgeText, ...(color ? [{ color }] : [])]}>
{children}
</Text>
</View>
);
};
@@ -0,0 +1,48 @@
import { Text, View, StyleSheet } from '@react-pdf/renderer';
import type { Style } from '@react-pdf/types';
type PdfPageNumberProps = {
style?: Style;
/**
* Format template for page number.
* Use {pageNumber} and {totalPages} as placeholders.
* Default: "{pageNumber} / {totalPages}"
*/
format?: string;
};
const styles = StyleSheet.create({
footer: {
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
position: 'absolute',
fontSize: 8,
bottom: 30,
left: 0,
right: 0,
textAlign: 'center',
color: 'grey',
},
});
export const PdfPageNumber = ({
style,
format = '{pageNumber} / {totalPages}',
}: PdfPageNumberProps) => {
return (
<View style={style || styles.footer} fixed>
<Text
render={({ pageNumber, totalPages }) =>
format
.replace('{pageNumber}', String(pageNumber))
.replace('{totalPages}', String(totalPages))
}
fixed
/>
</View>
);
};
+21 -14
View File
@@ -1,9 +1,10 @@
'use client';
import { View, StyleSheet } from '@react-pdf/renderer';
import { PdfThead, PdfColumn } from './PdfThead';
import { PdfTbody, PdfTbodyCell } from './PdfTbody';
import { PdfTfoot, PdfTfootCell } from './PdfTfoot';
import type { PdfColumn } from './types';
import { PdfThead } from './PdfThead';
import { PdfTbody } from './PdfTbody';
import { PdfTfoot } from './PdfTfoot';
const styles = StyleSheet.create({
table: {
@@ -13,10 +14,10 @@ const styles = StyleSheet.create({
},
});
interface PdfTableProps {
columns: PdfColumn[];
data: PdfTbodyCell[][];
footer?: PdfTfootCell[];
interface PdfTableProps<TData = Record<string, unknown>> {
columns: PdfColumn<TData>[];
data: TData[];
showFooter?: boolean;
footerLabel?: string;
firstRow?: {
valueKey: string;
@@ -26,20 +27,26 @@ interface PdfTableProps {
};
}
export const PdfTable = ({
export const PdfTable = <TData = Record<string, unknown>,>({
columns,
data,
footer,
showFooter = false,
footerLabel = 'Total',
firstRow,
}: PdfTableProps) => {
}: PdfTableProps<TData>) => {
// Check if any column has footer defined
const hasFooter =
showFooter || columns.some((col) => col.footer !== undefined);
return (
<View style={styles.table}>
<PdfThead columns={columns} />
<PdfTbody columns={columns} rows={data} firstRow={firstRow} />
{footer && footer.length > 0 && (
<PdfTfoot columns={columns} cells={footer} label={footerLabel} />
<PdfThead columns={columns} data={data} />
<PdfTbody columns={columns} data={data} firstRow={firstRow} />
{hasFooter && data.length > 0 && (
<PdfTfoot columns={columns} data={data} label={footerLabel} />
)}
</View>
);
};
export type { PdfColumn };
+44 -54
View File
@@ -1,22 +1,8 @@
'use client';
import { Text, View, StyleSheet } from '@react-pdf/renderer';
export interface PdfColumn {
key: string;
header: string;
flex: number;
align?: 'left' | 'center' | 'right';
}
export interface PdfTbodyCell {
key: string;
value: string | number | React.ReactNode;
align?: 'left' | 'center' | 'right';
color?: string;
formatAs?: 'text' | 'date' | 'currency' | 'number';
formatDate?: string;
}
import { ReactNode } from 'react';
import type { PdfColumn } from './types';
const styles = StyleSheet.create({
tableRow: {
@@ -71,21 +57,22 @@ const styles = StyleSheet.create({
},
});
interface PdfTbodyProps {
columns: PdfColumn[];
rows: PdfTbodyCell[][];
interface PdfTbodyProps<TData = Record<string, unknown>> {
columns: PdfColumn<TData>[];
data: TData[];
firstRow?: {
valueKey: string;
value: number;
align?: 'right';
color?: string;
};
formatDate?: (date: string, format: string) => string;
formatNumber?: (num: number) => string;
formatCurrency?: (num: number) => string;
}
export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
export const PdfTbody = <TData = Record<string, unknown>,>({
columns,
data,
firstRow,
}: PdfTbodyProps<TData>) => {
return (
<>
{/* First Row */}
@@ -93,17 +80,17 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
<View style={[styles.tableRow, styles.tableBorderBottom]}>
{columns.map((column, index) => {
const isLastColumn = index === columns.length - 1;
const isfirstRowColumn = column.key === firstRow.valueKey;
const align = column.align || 'center';
const isFirstRowColumn = column.key === firstRow.valueKey;
const align = column.align || 'left';
const cellStyle =
column.key === 'no'
? [styles.tableCellNo, { flex: column.flex }]
: isfirstRowColumn
? [styles.tableCellNo, { flex: column.flex || 1 }]
: isFirstRowColumn
? [
styles.tableCellRight,
{
flex: column.flex,
flex: column.flex || 1,
color: firstRow.color || 'black',
borderRightWidth: isLastColumn ? 0 : 1,
},
@@ -112,7 +99,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
? [
styles.tableCellRight,
{
flex: column.flex,
flex: column.flex || 1,
borderRightWidth: isLastColumn ? 0 : 1,
},
]
@@ -120,7 +107,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
? [
styles.tableCellCenter,
{
flex: column.flex,
flex: column.flex || 1,
borderRightWidth: isLastColumn ? 0 : 1,
},
]
@@ -128,15 +115,15 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
? [
styles.tableCellLast,
{
flex: column.flex,
flex: column.flex || 1,
borderRightWidth: 0,
},
]
: [styles.tableCell, { flex: column.flex }];
: [styles.tableCell, { flex: column.flex || 1 }];
return (
<View key={column.key} style={cellStyle}>
<Text>{isfirstRowColumn ? firstRow.value : ''}</Text>
<Text>{isFirstRowColumn ? firstRow.value : ''}</Text>
</View>
);
})}
@@ -144,8 +131,8 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
)}
{/* Data Rows */}
{rows.map((row, rowIndex) => {
const isLastRow = rowIndex === rows.length - 1;
{data.map((row, rowIndex) => {
const isLastRow = rowIndex === data.length - 1;
return (
<View
@@ -156,19 +143,27 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
]}
>
{columns.map((column, colIndex) => {
const cell = row.find((c) => c.key === column.key);
const isLastColumn = colIndex === columns.length - 1;
const align = cell?.align || column.align || 'center';
const align = column.align || 'left';
// Get cell content from column.cell function or fallback to row value
let cellContent: ReactNode;
if (column.cell) {
cellContent = column.cell({ row, index: rowIndex });
} else {
cellContent =
((row as Record<string, unknown>)[column.key] as ReactNode) ??
'-';
}
const cellStyle =
column.key === 'no'
? [styles.tableCellNo, { flex: column.flex }]
? [styles.tableCellNo, { flex: column.flex || 1 }]
: align === 'right'
? [
styles.tableCellRight,
{
flex: column.flex,
color: cell?.color || 'black',
flex: column.flex || 1,
borderRightWidth: isLastColumn ? 0 : 1,
},
]
@@ -176,37 +171,30 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
? [
styles.tableCellCenter,
{
flex: column.flex,
color: cell?.color || 'black',
flex: column.flex || 1,
borderRightWidth: isLastColumn ? 0 : 1,
},
]
: isLastColumn
? [
styles.tableCellLast,
{ flex: column.flex, borderRightWidth: 0 },
{ flex: column.flex || 1, borderRightWidth: 0 },
]
: [
styles.tableCell,
{
flex: column.flex,
color: cell?.color || 'black',
flex: column.flex || 1,
borderRightWidth: isLastColumn ? 0 : 1,
},
];
return (
<View key={column.key} style={cellStyle}>
{cell?.value !== undefined &&
cell?.value !== null &&
cell?.value !== '' ? (
typeof cell.value === 'object' ? (
cell.value
) : (
<Text>{String(cell.value)}</Text>
)
{typeof cellContent === 'string' ||
typeof cellContent === 'number' ? (
<Text>{String(cellContent)}</Text>
) : (
<Text>-</Text>
cellContent
)}
</View>
);
@@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
</>
);
};
export type { PdfColumn };
+48 -38
View File
@@ -1,21 +1,8 @@
'use client';
import { Text, View, StyleSheet } from '@react-pdf/renderer';
export interface PdfColumn {
key: string;
header: string;
flex: number;
align?: 'left' | 'center' | 'right';
}
export interface PdfTfootCell {
key: string;
value: string | number;
align?: 'left' | 'center' | 'right';
flex?: number;
color?: string;
}
import { ReactNode } from 'react';
import type { PdfColumn } from './types';
const styles = StyleSheet.create({
tableRow: {
@@ -69,63 +56,86 @@ const styles = StyleSheet.create({
},
});
interface PdfTfootProps {
columns: PdfColumn[];
cells: PdfTfootCell[];
interface PdfTfootProps<TData = Record<string, unknown>> {
columns: PdfColumn<TData>[];
data: TData[];
label?: string;
}
export const PdfTfoot = ({
export const PdfTfoot = <TData = Record<string, unknown>,>({
columns,
cells,
data,
label = 'Total',
}: PdfTfootProps) => {
}: PdfTfootProps<TData>) => {
return (
<View style={[styles.tableRow, styles.summaryRow]}>
{columns.map((column, index) => {
const isLastColumn = index === columns.length - 1;
const cellData = cells.find((c) => c.key === column.key);
// Get footer content from column definition
let footerContent: ReactNode;
if (typeof column.footer === 'function') {
footerContent = column.footer(data);
} else {
footerContent = column.footer;
}
// Use label for first column (usually 'no' column)
const displayContent = column.key === 'no' ? label : footerContent;
// Determine alignment
const align = column.footerAlign || column.align || 'left';
const color = column.footerColor || 'black';
const cellStyle =
column.key === 'no'
? [
styles.tableCellNo,
{ flex: column.flex, borderRightWidth: isLastColumn ? 0 : 1 },
{
flex: column.flex || 1,
borderRightWidth: isLastColumn ? 0 : 1,
color,
},
]
: cellData?.align === 'right'
: align === 'right'
? [
styles.tableCellRight,
{
flex: column.flex,
color: cellData?.color || 'black',
flex: column.flex || 1,
color,
borderRightWidth: isLastColumn ? 0 : 1,
},
]
: cellData?.align === 'center'
: align === 'center'
? [
styles.tableCellCenter,
{
flex: column.flex,
color: cellData?.color || 'black',
flex: column.flex || 1,
color,
borderRightWidth: isLastColumn ? 0 : 1,
},
]
: isLastColumn
? [styles.tableCellLast, { flex: column.flex }]
: [
styles.tableCell,
{
flex: column.flex,
color: cellData?.color || 'black',
},
];
? [styles.tableCellLast, { flex: column.flex || 1, color }]
: [styles.tableCell, { flex: column.flex || 1, color }];
return (
<View key={column.key} style={cellStyle}>
<Text>{column.key === 'no' ? label : cellData?.value || ''}</Text>
{displayContent !== undefined && displayContent !== null ? (
typeof displayContent === 'string' ||
typeof displayContent === 'number' ? (
<Text>{String(displayContent)}</Text>
) : (
displayContent
)
) : (
<Text>-</Text>
)}
</View>
);
})}
</View>
);
};
export type { PdfColumn };
+29 -14
View File
@@ -1,13 +1,8 @@
'use client';
import { Text, View, StyleSheet } from '@react-pdf/renderer';
export interface PdfColumn {
key: string;
header: string;
flex: number;
align?: 'left' | 'center' | 'right';
}
import { ReactNode } from 'react';
import type { PdfColumn } from './types';
const styles = StyleSheet.create({
tableRow: {
@@ -48,23 +43,37 @@ const styles = StyleSheet.create({
},
});
interface PdfTheadProps {
columns: PdfColumn[];
interface PdfTheadProps<TData = Record<string, unknown>> {
columns: PdfColumn<TData>[];
data?: TData[];
}
export const PdfThead = ({ columns }: PdfTheadProps) => {
export const PdfThead = <TData = Record<string, unknown>,>({
columns,
data,
}: PdfTheadProps<TData>) => {
return (
<View style={[styles.tableRow, styles.tableHeader]}>
{columns.map((column, index) => {
const align = column.align || 'center';
const isLastColumn = index === columns.length - 1;
// Get header content from column definition
let headerContent: ReactNode;
if (typeof column.header === 'function') {
headerContent = column.header(data || []);
} else {
headerContent = column.header || column.key;
}
// Determine alignment - columns align right by default for numeric data
const align = column.align || 'left';
const cellStyle =
align === 'right'
? [
styles.tableCellHeaderRight,
{
flex: column.flex,
flex: column.flex || 1,
textAlign: 'right' as const,
borderRightWidth: isLastColumn ? 0 : 1,
},
@@ -72,7 +81,7 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
: [
styles.tableCellHeader,
{
flex: column.flex,
flex: column.flex || 1,
textAlign: align as 'left' | 'center' | 'right',
borderRightWidth: isLastColumn ? 0 : 1,
},
@@ -80,10 +89,16 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
return (
<View key={column.key} style={cellStyle}>
<Text>{column.header}</Text>
{typeof headerContent === 'string' ? (
<Text>{headerContent}</Text>
) : (
headerContent
)}
</View>
);
})}
</View>
);
};
export type { PdfColumn };
+1 -3
View File
@@ -2,6 +2,4 @@ export { PdfTable } from './PdfTable';
export { PdfThead } from './PdfThead';
export { PdfTbody } from './PdfTbody';
export { PdfTfoot } from './PdfTfoot';
export type { PdfColumn } from './PdfThead';
export type { PdfTbodyCell } from './PdfTbody';
export type { PdfTfootCell } from './PdfTfoot';
export type { PdfColumn } from './types';
+24
View File
@@ -0,0 +1,24 @@
import { ReactNode } from 'react';
/**
* PdfColumn - Mirip dengan ColumnDef di TanStack Table
* Mengatur header (thead), body (tbody), dan footer (tfoot) dalam satu definisi
*/
export interface PdfColumn<TData = Record<string, unknown>> {
key: string;
flex?: number;
// Header configuration (thead)
header?: string | ((data: TData[]) => ReactNode);
// Body configuration (tbody)
align?: 'left' | 'center' | 'right';
cell?: (props: { row: TData; index: number }) => ReactNode | string | number;
// Footer configuration (tfoot)
footer?: string | number | ((data: TData[]) => ReactNode | string | number);
footerAlign?: 'left' | 'center' | 'right';
footerColor?: string;
}
export type { PdfColumn as default };
@@ -0,0 +1,80 @@
import { Color } from '@/types/theme';
import { Text, StyleSheet } from '@react-pdf/renderer';
import type { Style } from '@react-pdf/types';
type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label';
type TypographyVariant = Color | 'default';
type PdfTypographyProps = {
children: React.ReactNode;
size?: TypographySize;
variant?: TypographyVariant;
color?: string;
style?: Style;
};
const styles = StyleSheet.create({
h1: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
},
h2: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 8,
},
h3: {
fontSize: 10,
fontWeight: 'bold',
marginBottom: 4,
},
h4: {
fontSize: 9,
fontWeight: 'bold',
marginBottom: 3,
},
p: {
fontSize: 10,
marginBottom: 4,
},
small: {
fontSize: 8,
marginBottom: 2,
},
label: {
fontSize: 9,
marginBottom: 5,
},
});
const variantColors: Record<TypographyVariant, string> = {
default: '#333333',
primary: '#1f74bf',
secondary: '#6B7280',
accent: '#8B5CF6',
neutral: '#6B7280',
info: '#3B82F6',
success: '#065F46',
warning: '#92400E',
error: '#DC2626',
none: '#333333',
};
export const PdfTypography = ({
children,
size = 'p',
variant = 'default',
color,
style,
}: PdfTypographyProps) => {
const sizeStyle = styles[size];
const textColor = color || variantColors[variant];
return (
<Text style={[sizeStyle, { color: textColor }, ...(style ? [style] : [])]}>
{children}
</Text>
);
};
@@ -0,0 +1,65 @@
export type StatusColor = {
bg: string;
text: string;
border: string;
};
// Due status colors (for debt supplier reports)
export const dueStatusColors: Record<string, StatusColor> = {
'SUDAH JATUH TEMPO': {
bg: '#FEE2E2',
text: '#991B1B',
border: '#F87171',
}, // error/red
'BELUM JATUH TEMPO': {
bg: '#D1FAE5',
text: '#065F46',
border: '#34D399',
}, // success/green
'MENDEKATI JATUH TEMPO': {
bg: '#FEF3C7',
text: '#92400E',
border: '#FBBF24',
}, // warning/yellow
};
// Payment status colors (for customer payment & debt supplier reports)
export const paymentStatusColors: Record<string, StatusColor> = {
'BELUM LUNAS': {
bg: '#FEF3C7',
text: '#92400E',
border: '#FBBF24',
}, // warning/yellow
LUNAS: {
bg: '#DBEAFE',
text: '#1E40AF',
border: '#60A5FA',
}, // primary/blue
'PEMBAYARAN SEBAGIAN': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
PEMBAYARAN: {
bg: '#D1FAE5',
text: '#065F46',
border: '#34D399',
}, // success/green
};
// Fallback color for unknown statuses
export const fallbackStatusColor: StatusColor = {
bg: '#F3F4F6',
text: '#374151',
border: '#D1D5DB',
}; // neutral
export const getPDFBadgeStyle = (
statusText: string,
type: 'due' | 'payment' = 'payment'
): StatusColor => {
const normalizedStatus = statusText.toUpperCase().trim();
const colors =
type === 'due'
? dueStatusColors[normalizedStatus]
: paymentStatusColors[normalizedStatus];
return colors || fallbackStatusColor;
};
@@ -1,5 +1,4 @@
import IconSkeleton from '@/components/helper/skeleton/IconSkeleton';
import { Icon } from '@iconify/react';
const DataStateSkeleton = ({
icon,
-1
View File
@@ -4,7 +4,6 @@ import { ChangeEvent } from 'react';
import {
PatternFormat,
NumberFormatBase,
NumberFormatBaseProps,
OnValueChange,
} from 'react-number-format';
import TextInput, { TextInputProps } from '@/components/input/TextInput';
+33 -38
View File
@@ -246,8 +246,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
className={cn(
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
{
'bg-gray-100 border-base-content/10': !isDisabled,
'bg-gray-50 border-base-content/10': isDisabled,
'bg-base-100 border-base-content/10': !isDisabled,
'bg-base-200 border-base-content/10': isDisabled,
'border-error': isError,
},
className?.inputPrefix
@@ -278,28 +278,28 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
className={cn('w-full flex-1', className?.select)}
classNames={{
control: ({ isFocused, isDisabled }) =>
cn('w-full border bg-white transition-shadow', 'rounded-lg!', {
'cursor-pointer!': !readOnly && !isDisabled,
'border-red-500! ring-2 ring-red-200': isError,
'border-indigo-500 ring-2 ring-indigo-200':
isFocused && !startAdornment,
'border-base-content/10!': !isError && !isFocused,
'bg-gray-100 text-gray-400 cursor-not-allowed':
cn('w-full border transition-shadow', 'rounded-lg!', {
'bg-base-100!': !isDisabled && !readOnly,
'bg-base-200! text-gray-400 cursor-not-allowed':
isDisabled && !readOnly,
'bg-transparent! cursor-not-allowed!': readOnly,
'cursor-pointer!': !readOnly && !isDisabled,
'border-error!': isError,
'ring-2 ring-error/20': isError,
'border-indigo-500 ring-2 ring-indigo-200':
isFocused && !startAdornment && !isError,
'border-base-content/10!': !isError && !isFocused,
'rounded-l-none!': inputPrefix && !startAdornment,
'rounded-r-none!': inputSuffix && !startAdornment,
}),
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
placeholder: () =>
cn({
'text-gray-400 text-sm leading-tight': !isError,
'text-red-300!': isError,
cn('text-gray-400 text-sm leading-tight', {
'text-error!': isError,
}),
singleValue: () =>
cn({
'm-0! text-gray-900 text-sm leading-tight': !isError,
'text-error!': isError,
cn('m-0! text-gray-900 text-sm leading-tight', {
'text-error!': isError && !readOnly,
'text-gray-900!': readOnly,
}),
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
@@ -370,8 +370,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
className={cn(
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
{
'bg-gray-100 border-base-content/10': !isDisabled,
'bg-gray-50 border-base-content/10': isDisabled,
'bg-base-100 border-base-content/10': !isDisabled,
'bg-base-200 border-base-content/10': isDisabled,
'border-error': isError,
},
className?.inputSuffix
@@ -403,31 +403,26 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
className={cn('w-full', className?.select)}
classNames={{
control: ({ isFocused, isDisabled }) =>
cn(
'w-full border bg-white transition-shadow',
// Gunakan rounded-lg untuk semua kasus
'rounded-lg!',
{
'cursor-pointer!': !readOnly && !isDisabled,
'border-red-500! ring-2 ring-red-200': isError,
'border-indigo-500 ring-2 ring-indigo-200':
isFocused && !startAdornment,
'border-base-content/10!': !isError && !isFocused,
'bg-gray-100 text-gray-400 cursor-not-allowed':
isDisabled && !readOnly,
'bg-transparent! cursor-not-allowed!': readOnly,
}
),
cn('w-full border transition-shadow rounded-lg!', {
'bg-base-100!': !isDisabled && !readOnly,
'bg-base-200! text-gray-400 cursor-not-allowed':
isDisabled && !readOnly,
'bg-transparent! cursor-not-allowed!': readOnly,
'cursor-pointer!': !readOnly && !isDisabled,
'border-error!': isError,
'ring-2 ring-error/20': isError,
'border-indigo-500 ring-2 ring-indigo-200':
isFocused && !startAdornment && !isError,
'border-base-content/10!': !isError && !isFocused,
}),
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
placeholder: () =>
cn({
'text-gray-400 text-sm leading-tight': !isError,
'text-red-300!': isError,
cn('text-gray-400 text-sm leading-tight', {
'text-error!': isError,
}),
singleValue: () =>
cn({
'm-0! text-gray-900 text-sm leading-tight': !isError,
'text-error!': isError,
cn('m-0! text-gray-900 text-sm leading-tight', {
'text-error!': isError && !readOnly,
'text-gray-900!': readOnly,
}),
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
+18 -8
View File
@@ -104,8 +104,8 @@ const TextInput = ({
className={cn(
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
{
'bg-gray-100 border-base-content/10': !disabled,
'bg-gray-50 border-base-content/10': disabled,
'bg-base-100 border-base-content/10': !disabled,
'bg-base-200 border-base-content/10': disabled,
'border-error': isError,
'border-success!': isValid,
},
@@ -118,7 +118,7 @@ const TextInput = ({
<div
className={cn(
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white border-base-content/10',
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
{
'border-error': isError,
'border-success!': isValid,
@@ -126,7 +126,8 @@ const TextInput = ({
'rounded-r-none!': inputSuffix,
'input-disabled': disabled,
'cursor-not-allowed': disabled,
'bg-gray-50': disabled,
'bg-base-100': !disabled,
'bg-base-200': disabled,
},
className?.inputWrapper
)}
@@ -167,8 +168,8 @@ const TextInput = ({
className={cn(
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
{
'bg-gray-100 border-base-content/10': !disabled,
'bg-gray-50 border-base-content/10': disabled,
'bg-base-100 border-base-content/10': !disabled,
'bg-base-200 border-base-content/10': disabled,
'border-error': isError,
'border-success!': isValid,
},
@@ -182,10 +183,12 @@ const TextInput = ({
) : (
<div
className={cn(
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white border-base-content/10',
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
{
'border-error': isError,
'border-success!': isValid,
'bg-base-100': !disabled,
'bg-base-200': disabled,
},
className?.inputWrapper
)}
@@ -201,7 +204,14 @@ const TextInput = ({
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('grow', className?.input)}
className={cn(
'grow bg-transparent outline-none',
{
'cursor-not-allowed': disabled,
'text-gray-500': disabled,
},
className?.input
)}
readOnly={readOnly}
/>
@@ -56,7 +56,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
closeOnBackdrop={closeOnBackdrop}
primaryButton={{
...primaryButton,
onClick: (e) => {
onClick: () => {
if (primaryButton && primaryButton?.onClick) {
primaryButton?.onClick?.(notes);
} else {
@@ -5,28 +5,23 @@ import { useMemo, useState } from 'react';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import Tabs from '@/components/Tabs';
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent';
import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent';
import ClosingGeneralInformationTable from '@/components/pages/closing/table/ClosingGeneralInformationTable';
import SapronakClosingTab from '@/components/pages/closing/tab/SapronakClosingTab';
import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionDataClosingTab';
import {
ClosingGeneralInformation,
BaseClosingSales,
ClosingHppExpedition,
} from '@/types/api/closing';
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable';
import { ClosingGeneralInformation } from '@/types/api/closing';
import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab';
import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab';
import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab';
import SalesClosingTab from '@/components/pages/closing/tab/SalesClosingTab';
import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditionClosingTab';
import ClosingKandangList from '@/components/pages/closing/ClosingKandangList';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { useClosingTabStore } from '@/stores/closing/closing-tab.store';
interface ClosingDetailProps {
id: number;
initialValue?: ClosingGeneralInformation;
salesData?: BaseClosingSales;
hppExpeditionData?: ClosingHppExpedition;
projectData?: ProjectFlock;
kandangData?: ProjectFlockKandang;
}
@@ -34,25 +29,24 @@ interface ClosingDetailProps {
const ClosingDetail: React.FC<ClosingDetailProps> = ({
id,
initialValue,
salesData,
hppExpeditionData,
projectData,
kandangData,
}) => {
const [activeTab, setActiveTab] = useState<string>('sapronak');
const [activeTabId, setActiveTabId] = useState<string>('sapronak');
const tabActions = useClosingTabStore((state) => state.tabActions);
const closingDetailTabs = useMemo(() => {
const validTabs = [
{
id: 'sapronak',
label: 'Sapronak',
content: <ClosingSapronakTabContent projectFlockId={id} />,
content: <SapronakClosingTab projectFlockId={id} />,
},
{
id: 'perhitunganSapronak',
label: 'Perhitungan Sapronak',
content: (
<ClosingSapronakCalculationTabContent
<SapronakCalculationClosingTab
closingGeneralInformation={initialValue}
projectFlockId={id}
/>
@@ -61,13 +55,13 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
{
id: 'penjualan',
label: 'Penjualan',
content: <SalesReportTable initialValues={salesData} />,
content: <SalesClosingTab projectFlockId={id} />,
},
{
id: 'overhead',
label: 'Overhead',
content: (
<ClosingOverheadTabContent
<OverheadClosingTab
projectFlockId={id}
generalInformation={initialValue}
kandangData={kandangData}
@@ -77,26 +71,26 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
{
id: 'hppEkspedisi',
label: 'HPP Ekspedisi',
content: <HppExpeditionReportTable initialValues={hppExpeditionData} />,
content: <HppExpeditionClosingTab projectFlockId={id} />,
},
{
id: 'dataProduksi',
label: 'Data Produksi',
content: <ClosingProductionDataTabContent projectFlockId={id} />,
content: <ProductionDataClosingTab projectFlockId={id} />,
},
{
id: 'keuangan',
label: 'Keuangan',
content: <ClosingFinanceTabContent projectFlockId={id} />,
content: <FinanceClosingTab projectFlockId={id} />,
},
];
return validTabs;
}, [initialValue]);
}, [initialValue, kandangData, id]);
return (
<>
<section className='w-full max-w-7xl pb-16'>
<section className='w-full'>
<header className='flex flex-col gap-4'>
<Button
href={
@@ -126,13 +120,17 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
)}
<Tabs
activeTabId={activeTab}
onTabChange={setActiveTab}
activeTabId={activeTabId}
onTabChange={setActiveTabId}
tabs={closingDetailTabs}
variant='lifted'
variant='boxed'
className={{
wrapper: 'w-full mt-4',
tabHeaderWrapper:
'relative justify-between items-center py-3 before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10',
tab: 'w-fit',
content: 'p-0 m-0',
}}
sideContent={tabActions[activeTabId] || null}
/>
</section>
</>
@@ -1,17 +0,0 @@
import ClosingFinanceTable from '@/components/pages/closing/ClosingFinanceTable';
const ClosingFinanceTabContent = ({
projectFlockId,
}: {
projectFlockId: number;
}) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<ClosingFinanceTable projectFlockId={projectFlockId} />
)}
</div>
);
};
export default ClosingFinanceTabContent;
@@ -1,399 +0,0 @@
import Card from '@/components/Card';
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { formatCurrency, formatTitleCase } from '@/lib/helper';
import { ClosingApi } from '@/services/api/closing';
import { HppItem, ProfitLossItem } from '@/types/api/closing';
import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import useSWR from 'swr';
const ClosingFinanceTable = ({
projectFlockId,
}: {
projectFlockId: number;
}) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: finance, isLoading } = useSWR(
`/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
() =>
ClosingApi.getFinance(
projectFlockId,
kandangId ? Number(kandangId) : undefined
)
);
const hppTableData: HppItem[] = useMemo(() => {
if (isResponseSuccess(finance)) {
const customItems = {
label: 'HPP dan Pengeluaran',
code: 'custom_row',
} as HppItem;
const purchases = finance.data.hpp.items.filter(
(item) => item.category === 'purchase'
);
const totalBudgeting = {
label: 'HPP dan Bahan Baku',
code: 'custom_row',
} as HppItem;
const overheads = finance.data.hpp.items.filter(
(item) => item.category === 'overhead'
);
return [customItems, ...purchases, totalBudgeting, ...overheads];
}
return [];
}, [finance]);
const profitLossTableData: ProfitLossItem[] = useMemo(() => {
if (isResponseSuccess(finance)) {
const incomes = finance.data.profit_loss.items.filter(
(item) => item.type === 'income'
);
const purchases = finance.data.profit_loss.items.filter(
(item) => item.type === 'purchase'
);
const overheads = finance.data.profit_loss.items.filter(
(item) => item.type === 'overhead'
);
const grossProfit = {
label: 'LABA RUGI BRUTO',
code: 'custom_row',
type: 'gross_profit',
rp_per_bird:
finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
} as ProfitLossItem;
const subtotal = {
label: 'Subtotal',
code: 'custom_row',
type: 'subtotal',
rp_per_bird:
finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
} as ProfitLossItem;
return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
}
return [];
}, [finance]);
return (
<div className='flex flex-col gap-4'>
<>
<Card
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<div className='grid grid-cols-2 gap-6'>
<div className='flex flex-col gap-1'>
<div>Laba Rugi Brutto</div>
<div className='text-lg font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.gross_profit.amount
)
: '-'}
</div>
</div>
<div className='flex flex-col gap-1'>
<div>Laba Rugi Netto</div>
<div className='text-lg font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit.amount
)
: '-'}
</div>
</div>
</div>
</Card>
<Card
title='HPP Purchases'
variant='bordered'
collapsible
className={{
wrapper: 'w-full',
}}
>
<div className='mt-6 p-0 mb-0'>
<Table<HppItem>
data={hppTableData}
isLoading={isLoading}
columns={[
{
header: 'No.',
enableSorting: false,
accessorFn: (item, index) => {
if (item.code === 'custom_row') return '-';
const dataRowsBefore = hppTableData
.slice(0, index)
.filter((row) => row.code !== 'custom_row').length;
return dataRowsBefore + 1;
},
footer: (props) => {
return 'HPP';
},
},
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => formatTitleCase(item.label || '-'),
},
{
header: 'Budgeting',
enableSorting: false,
columns: [
{
header: 'Rp/Ekor',
id: 'budgeting_rp_per_bird',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.rp_per_bird || 0),
footer: (props) => {
return props.column.id === 'budgeting_rp_per_bird' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting
?.rp_per_bird || 0
)
: '-';
},
},
{
header: 'Rp/Kg',
id: 'budgeting_rp_per_kg',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.rp_per_kg || 0),
footer: (props) => {
return props.column.id === 'budgeting_rp_per_kg' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting?.rp_per_kg ||
0
)
: '-';
},
},
{
header: 'Jumlah (Rp)',
id: 'budgeting_amount',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.amount || 0),
footer: (props) => {
return props.column.id === 'budgeting_amount' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting?.amount || 0
)
: '-';
},
},
],
},
{
header: 'Realization',
enableSorting: false,
columns: [
{
header: 'Rp/Ekor',
id: 'realization_rp_per_bird',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.rp_per_bird || 0),
footer: (props) => {
return props.column.id === 'realization_rp_per_bird' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization
?.rp_per_bird || 0
)
: '-';
},
},
{
header: 'Rp/Kg',
id: 'realization_rp_per_kg',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.rp_per_kg || 0),
footer: (props) => {
return props.column.id === 'realization_rp_per_kg' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization
?.rp_per_kg || 0
)
: '-';
},
},
{
header: 'Jumlah (Rp)',
id: 'realization_amount',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.amount || 0),
footer: (props) => {
return props.column.id === 'realization_amount' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization?.amount || 0
)
: '-';
},
},
],
},
]}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.code === 'custom_row') {
return (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
></td>
<td
colSpan={7}
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
</tr>
);
}
return null;
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
<Card
title='Profit/Loss'
variant='bordered'
collapsible
className={{
wrapper: 'w-full',
}}
>
<div className='mt-6 p-0 mb-0'>
<Table<ProfitLossItem>
data={profitLossTableData}
isLoading={isLoading}
columns={[
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => item.label,
cell: (item) => (
<div className=''>
{formatTitleCase(item.row.original.label || '-')}
</div>
),
footer: () => (
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
),
},
{
header: 'Rp/Ekor',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_bird || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Rp/Kg',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_kg || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Jumlah (Rp)',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.amount || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.amount || 0
)
: formatCurrency(0)}
</div>
),
},
]}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.code === 'custom_row') {
return (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.footerRowClassName}
>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold ps-6 uppercase'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_bird ?? 0)}
</div>
</td>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_kg ?? 0)}
</div>
</td>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold'>
{formatCurrency(rowData.amount ?? 0)}
</div>
</td>
</tr>
);
}
return null;
}}
className={{
paginationClassName: 'hidden',
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
</>
</div>
);
};
export default ClosingFinanceTable;
@@ -10,18 +10,18 @@ const ClosingKandangList = ({
projectData?: ProjectFlock;
}) => {
return (
<div className='w-full my-4 @container'>
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
<div className='flex flex-col @sm:flex-row gap-4'>
<div className='w-full'>
<div className='overflow-x-auto'>
<h1 className='font-bold my-4'>Kandang</h1>
<div className='flex flex-wrap gap-2 mb-4'>
<h1 className='font-bold mb-3'>Kandang</h1>
<div className='flex flex-wrap gap-2'>
{projectData?.kandangs?.map((kandang) => (
<Button
key={kandang.id}
variant='outline'
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
className='min-w-32'
>
{kandang.name}
</Button>
@@ -1,308 +0,0 @@
'use client';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { ClosingApi } from '@/services/api/closing';
import { isResponseSuccess } from '@/lib/api-helper';
import { formatNumber } from '@/lib/helper';
interface ClosingProductionDataTabContentProps {
projectFlockId: number;
}
const ClosingProductionDataTabContent = ({
projectFlockId,
}: ClosingProductionDataTabContentProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: productionData, isLoading } = useSWR(
`${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
() => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
);
if (isLoading) {
return (
<div className='w-full flex justify-center py-8'>
<span className='loading loading-spinner loading-lg' />
</div>
);
}
if (!productionData || !isResponseSuccess(productionData)) {
return (
<div className='w-full text-center py-8 text-gray-500'>
Gagal memuat data produksi.
</div>
);
}
const { purchase, sales, performance } = productionData.data;
// Helper for consistent row styling
const DataRow = ({
label,
value,
unit = '',
valueClassName = 'font-bold text-gray-800',
unitClassName = 'text-gray-500 w-12 text-right',
}: {
label: string;
value: string | number;
unit?: string;
valueClassName?: string;
unitClassName?: string;
}) => (
<div className='flex justify-between items-center py-1'>
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
<div className='flex gap-2 w-1/2 justify-end items-center'>
<span className={valueClassName}>{value}</span>
{unit && <span className={unitClassName}>{unit}</span>}
</div>
</div>
);
return (
<div className='w-full rounded-xl p-8 shadow-sm'>
<h2 className='text-lg font-bold mb-8 text-gray-800'>Data Produksi</h2>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
{/* Left Column */}
<div className='space-y-10'>
{/* Purchase Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Pembelian
</h3>
<div className='space-y-1'>
<DataRow
label='Populasi Awal'
value={formatNumber(purchase.initial_population)}
unit='Ekor'
/>
<DataRow
label='Claim Culling'
value={formatNumber(purchase.claim_culling)}
unit='Ekor'
/>
<DataRow
label='Populasi Akhir'
value={formatNumber(purchase.final_population)}
unit='Ekor'
/>
<DataRow
label='Pakan Masuk'
value={formatNumber(purchase.feed_in)}
unit='Kg'
/>
<DataRow
label='Pakan Terpakai'
value={formatNumber(purchase.feed_used)}
unit='Kg'
/>
</div>
</section>
{/* Sales Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Penjualan
</h3>
<div className='space-y-4'>
{/* Chicken Sales */}
<div className='space-y-1'>
<DataRow
label='Penjualan (Ekor)'
value={formatNumber(sales.chicken.sales_population)}
unit='Ekor'
/>
<DataRow
label='Penjualan (Kg)'
value={formatNumber(sales.chicken.sales_weight)}
unit='Kg'
/>
<DataRow
label='Bobot Rata-Rata'
value={formatNumber(sales.chicken.avg_weight)}
unit='Kg/Ekor'
/>
<DataRow
label='Harga Jual Rata-Rata'
value={formatNumber(sales.chicken.avg_selling_price)}
unit='Rupiah'
/>
</div>
{/* Egg Sales (if available) */}
{sales.egg && (
<>
<div className='h-px bg-gray-100 my-2' />
<div className='space-y-1'>
<DataRow
label='Telur (Butir)'
value={formatNumber(sales.egg.egg_pieces)}
unit='Butir'
/>
<DataRow
label='Telur (Kg)'
value={formatNumber(sales.egg.egg_mass)}
unit='Kg'
/>
<DataRow
label='Berat Telur Rata-Rata'
value={formatNumber(sales.egg.avg_egg_weight)}
unit='Kg'
/>
<DataRow
label='Harga Jual Telur Rata-Rata'
value={formatNumber(sales.egg.avg_selling_price)}
unit='Rupiah'
/>
</div>
</>
)}
</div>
</section>
</div>
{/* Divider Line (Absolute centered) */}
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
{/* Right Column */}
<div className='space-y-10 flex flex-col h-full'>
{/* Performance Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Performance
</h3>
<div className='space-y-1'>
<DataRow
label='Deplesi'
value={formatNumber(performance.depletion)}
unit='Ekor'
/>
<DataRow
label='Umur'
value={formatNumber(performance.age_day)}
unit='Hari'
/>
<DataRow
label='Mortalitas Std'
value={formatNumber(performance.mor_std)}
unitClassName='hidden'
/>
<DataRow
label='Mortalitas Act'
value={formatNumber(performance.mor_act)}
unitClassName='hidden'
/>
<DataRow
label='DEFF Mortalitas'
value={formatNumber(performance.mor_diff)}
unitClassName='hidden'
/>
{/* <DataRow
label='AWG Std'
value={formatNumber(performance.awg_std)}
unit='Gr/Hari'
/>
<DataRow
label='AWG Act'
value={formatNumber(performance.awg_act)}
unit='Gr/Hari'
/> */}
<DataRow
label='Feed Intake Std'
value={formatNumber(performance.feed_intake_std)}
unitClassName='hidden'
/>
<DataRow
label='Feed Intake Act'
value={formatNumber(performance.feed_intake)}
unitClassName='hidden'
/>
<DataRow
label='FCR Std'
value={formatNumber(performance.fcr_std)}
unitClassName='hidden'
/>
<DataRow
label='FCR Act'
value={formatNumber(performance.fcr_act)}
unitClassName='hidden'
/>
<DataRow
label='DEFF FCR'
value={formatNumber(performance.fcr_diff)}
unitClassName='hidden'
/>
{/* Laying Specific Fields */}
{performance.hen_day_act !== undefined && (
<>
<DataRow
label='Hen Day Std'
value={formatNumber(performance.hen_day_std!)}
unit='%'
/>
<DataRow
label='Hen Day Act'
value={formatNumber(performance.hen_day_act)}
unit='%'
/>
</>
)}
{performance.egg_mass !== undefined && (
<>
<DataRow
label='Egg Mass Std'
value={formatNumber(performance.egg_mass_std!)}
unit='Kg'
/>
<DataRow
label='Egg Mass Act'
value={formatNumber(performance.egg_mass)}
unit='Kg'
/>
</>
)}
{performance.egg_weight !== undefined && (
<>
<DataRow
label='Egg Weight Std'
value={formatNumber(performance.egg_weight_std!)}
unit='Gr'
/>
<DataRow
label='Egg Weight Act'
value={formatNumber(performance.egg_weight)}
unit='Gr'
/>
</>
)}
{performance.hen_housed_act !== undefined && (
<>
<DataRow
label='Hen Housed Std'
value={formatNumber(performance.hen_housed_std!)}
unit='%'
/>
<DataRow
label='Hen Housed Act'
value={formatNumber(performance.hen_housed_act)}
unit='%'
/>
</>
)}
</div>
</section>
</div>
</div>
</div>
);
};
export default ClosingProductionDataTabContent;
@@ -1,268 +0,0 @@
'use client';
import Card from '@/components/Card';
import Table from '@/components/Table';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import {
RowSapronakCalculation,
TotalSapronakCalculation,
} from '@/types/api/closing';
import { ColumnDef } from '@tanstack/react-table';
import { useMemo } from 'react';
import useSWR from 'swr';
import { ClosingApi } from '@/services/api/closing';
import { isResponseSuccess } from '@/lib/api-helper';
import { ClosingGeneralInformation } from '@/types/api/closing';
import { useSearchParams } from 'next/navigation';
interface ClosingSapronakCalculationTableProps {
projectFlockId: number;
closingGeneralInformation?: ClosingGeneralInformation;
}
const ClosingSapronakCalculationTable = ({
projectFlockId,
closingGeneralInformation,
}: ClosingSapronakCalculationTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: sapronakCalculation, isLoading } = useSWR(
`/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
() => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)),
{
keepPreviousData: true,
}
);
// Helper function to create columns with footer support
const createColumns = (
total?: TotalSapronakCalculation
): ColumnDef<RowSapronakCalculation>[] => [
{
header: 'Tanggal',
accessorKey: 'date',
cell: (props) =>
props.row.original.date
? formatDate(props.row.original.date, 'DD MMM YYYY')
: '-',
footer: 'Total',
},
{
header: 'No. Referensi',
accessorKey: 'reference_number',
cell: (props) => (props.row.original.reference_number as string) || '-',
footer: '',
},
{
header: 'QTY Masuk',
accessorKey: 'qty_in',
cell: (props) =>
props.row.original.qty_in
? formatNumber(props.row.original.qty_in as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_in ? formatNumber(total?.qty_in) : '0'}
</div>
)
: '',
},
{
header: 'QTY Keluar',
accessorKey: 'qty_out',
cell: (props) =>
props.row.original.qty_out
? formatNumber(props.row.original.qty_out as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_out ? formatNumber(total?.qty_out) : '0'}
</div>
)
: '',
},
{
header: 'QTY Pakai',
accessorKey: 'qty_used',
cell: (props) =>
props.row.original.qty_used
? formatNumber(props.row.original.qty_used as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_used ? formatNumber(total?.qty_used) : '0'}
</div>
)
: '',
},
{
header: 'Uraian',
accessorKey: 'description',
cell: (props) => (props.row.original.description as string) || '-',
footer: '',
},
{
header: 'Kategori Produk',
accessorKey: 'product_category',
cell: (props) => (props.row.original.product_category as string) || '-',
footer: '',
},
{
header: 'Harga Beli/Qty (Rp)',
accessorKey: 'unit_price',
cell: (props) =>
props.row.original.unit_price
? formatCurrency(props.row.original.unit_price as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.avg_unit_price
? formatCurrency(total?.avg_unit_price)
: '-'}
</div>
)
: '',
},
{
header: 'Total Harga (Rp)',
accessorKey: 'total_amount',
cell: (props) =>
props.row.original.total_amount
? formatCurrency(props.row.original.total_amount as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.total_amount ? formatCurrency(total?.total_amount) : '-'}
</div>
)
: '',
},
{
header: 'Keterangan',
accessorKey: 'notes',
cell: (props) => (props.row.original.notes as string) || '-',
footer: '',
},
];
// Memoize columns untuk setiap kategori
const docColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.doc?.total)
: createColumns(),
[sapronakCalculation]
);
const ovkColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.ovk?.total)
: createColumns(),
[sapronakCalculation]
);
const pakanColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.pakan?.total)
: createColumns(),
[sapronakCalculation]
);
return (
<div className='flex flex-col gap-4'>
{/* Table DOC jika kategori Project Flock Growing */}
<Card
title={
closingGeneralInformation?.project_type == 'GROWING'
? 'DOC'
: 'Pullet'
}
collapsible
defaultCollapsed={false}
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.doc?.rows ?? [])
: []
}
columns={docColumns}
className={{
containerClassName: 'my-4',
}}
renderFooter={
isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.doc?.rows.length > 0
}
/>
</Card>
<Card
title='OVK'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full',
}}
>
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.ovk?.rows ?? [])
: []
}
columns={ovkColumns}
className={{
containerClassName: 'my-4',
}}
renderFooter={
isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.ovk?.rows.length > 0
}
/>
</Card>
<Card
title='Pakan'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full',
}}
>
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.pakan?.rows ?? [])
: []
}
columns={pakanColumns}
className={{
containerClassName: 'my-4',
}}
renderFooter={
isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.pakan?.rows.length > 0
}
/>
</Card>
</div>
);
};
export default ClosingSapronakCalculationTable;
@@ -1,36 +0,0 @@
'use client';
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable';
import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable';
interface ClosingSapronakTableProps {
projectFlockId?: number;
}
const ClosingSapronakTabContent = ({
projectFlockId,
}: ClosingSapronakTableProps) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<>
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
<ClosingIncomingSapronaksSummaryTable
projectFlockId={projectFlockId}
/>
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
<ClosingOutgoingSapronaksSummaryTable
projectFlockId={projectFlockId}
/>
</>
)}
</div>
);
};
export default ClosingSapronakTabContent;
+362 -142
View File
@@ -1,68 +1,116 @@
'use client';
import { ChangeEventHandler, useEffect, useState } from 'react';
import { ChangeEventHandler, useEffect, useState, useMemo } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { useRouter } from 'next/navigation';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import SelectInput, { useSelect } from '@/components/input/SelectInput';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge';
import Modal, { useModal } from '@/components/Modal';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import { useFormik } from 'formik';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { LocationApi } from '@/services/api/master-data';
import { Location } from '@/types/api/master-data/location';
import { ClosingApi } from '@/services/api/closing';
import { Closing } from '@/types/api/closing';
const PROJECT_STATUS_OPTIONS = [
{
value: 1,
label: 'Pengajuan',
},
{
value: 2,
label: 'Aktif',
},
];
import { Color } from '@/types/theme';
import {
ClosingFilterSchema,
ClosingFilterType,
} from '@/components/pages/closing/filter/ClosingFilter';
import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton';
const RowOptionsMenu = ({
type = 'dropdown',
props,
popoverPosition = 'bottom',
detailClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Closing, unknown>;
popoverPosition: 'bottom' | 'top';
detailClickHandler: (id: number) => void;
}) => {
const popoverId = `closing#${props.row.original.id}`;
const popoverAnchorName = `--anchor-closing#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
const detailClickHandlerWrapper = () => {
detailClickHandler(props.row.original.id);
closePopover();
};
return (
<RowOptionsMenuWrapper type={type}>
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
<RequirePermission permissions='lti.closing.detail'>
<Button
href={`/closing/detail/?closingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
</div>
</RowOptionsMenuWrapper>
<div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.closing.detail'>
<Button
variant='ghost'
color='none'
onClick={detailClickHandlerWrapper}
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:eye' width={20} height={20} />
View Details
</Button>
</RequirePermission>
</div>
</PopoverContent>
</div>
);
};
const ClosingsTable = () => {
// ===== ROUTER =====
const router = useRouter();
// ===== STATUS BADGE COLOR HELPER =====
const getProjectStatusBadgeColor = (status: string): Color => {
const normalizedValue = status.toLowerCase();
if (normalizedValue === 'aktif') {
return 'success';
}
if (normalizedValue === 'pengajuan') {
return 'neutral';
}
return 'neutral';
};
// ===== FILTER MODAL STATE =====
const filterModal = useModal();
const {
state: tableFilterState,
updateFilter,
@@ -72,36 +120,67 @@ const ClosingsTable = () => {
} = useTableFilter({
initial: {
search: '',
nameSort: '',
transactionDate: '',
realizationDate: '',
locationId: '',
projectStatus: '',
userId: '',
// nameSort: '',
// transactionDate: '',
// realizationDate: '',
location_id: '',
project_status: '',
// userId: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
nameSort: 'sort_name',
transactionDate: 'transaction_date',
realizationDate: 'realization_date',
locationId: 'location_id',
projectStatus: 'project_status',
userId: 'user_id',
// nameSort: 'sort_name',
// transactionDate: 'transaction_date',
// realizationDate: 'realization_date',
// locationId: 'location_id',
// projectStatus: 'project_status',
// userId: 'user_id',
search: 'search',
location_id: 'location_id',
project_status: 'project_status',
},
});
// ===== FORMIK SETUP =====
const formik = useFormik<ClosingFilterType>({
initialValues: {
location_id: null,
project_status: null,
},
validationSchema: ClosingFilterSchema,
onSubmit: (values, { setSubmitting }) => {
updateFilter('location_id', values.location_id || '');
updateFilter('project_status', values.project_status || '');
filterModal.closeModal();
setSubmitting(false);
},
onReset: () => {
updateFilter('location_id', '');
updateFilter('project_status', '');
},
});
// ===== DATA FETCHING =====
const { data: closings, isLoading: isLoadingClosings } = useSWR(
`${ClosingApi.basePath}${getTableFilterQueryString()}`,
ClosingApi.getAllFetcher
);
const data = useMemo(
() =>
isResponseSuccess(closings) ? (closings?.data as Closing[]) || [] : [],
[closings]
);
// ===== PAGINATION & STATE =====
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
// ===== TABLE COLUMNS =====
const closingsColumns: ColumnDef<Closing>[] = [
{
header: '#',
header: 'No',
cell: (props) => props.row.index + 1,
},
{
@@ -133,6 +212,19 @@ const ClosingsTable = () => {
{
accessorKey: 'project_status',
header: 'Status',
cell: (props) => {
const status = props.row.original.project_status;
const badgeColor = getProjectStatusBadgeColor(status);
return (
<StatusBadge
color={badgeColor}
text={status}
className={{
badge: 'whitespace-nowrap',
}}
/>
);
},
},
{
header: 'Aksi',
@@ -142,27 +234,24 @@ const ClosingsTable = () => {
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const detailClickHandler = (id: number) => {
router.push(`/closing/detail/?closingId=${id}`);
};
return (
<>
{currentPageSize > 3 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu type='dropdown' props={props} />
</RowDropdownOptions>
)}
{currentPageSize <= 3 && (
<RowCollapseOptions>
<RowOptionsMenu type='collapse' props={props} />
</RowCollapseOptions>
)}
</>
<RowOptionsMenu
props={props}
detailClickHandler={detailClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
];
// ===== LOCATION OPTIONS =====
const {
setInputValue: setLocationInputValue,
options: locationOptions,
@@ -170,115 +259,246 @@ const ClosingsTable = () => {
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null
// ===== PROJECT STATUS OPTIONS =====
const projectStatusOptions = useMemo(
() => [
{ value: '1', label: 'Pengajuan' },
{ value: '2', label: 'Aktif' },
],
[]
);
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedLocation(val as OptionType);
updateFilter(
'locationId',
val ? ((val as OptionType).value as string) : ''
// ===== FILTER HELPERS =====
const locationIdValue = useMemo(() => {
if (!formik.values.location_id) return null;
return (
locationOptions.find(
(opt) => String(opt.value) === formik.values.location_id
) || null
);
};
}, [formik.values.location_id, locationOptions]);
const [selectedProjectStatus, setSelectedProjectStatus] =
useState<OptionType | null>(null);
const projectStatusChangeHandler = (
val: OptionType | OptionType[] | null
) => {
setSelectedProjectStatus(val as OptionType);
updateFilter(
'projectStatus',
val ? ((val as OptionType).value as string) : ''
const projectStatusValue = useMemo(() => {
if (!formik.values.project_status) return null;
return (
projectStatusOptions.find(
(opt) => opt.value === formik.values.project_status
) || null
);
};
}, [formik.values.project_status, projectStatusOptions]);
// ===== ACTIVE FILTERS COUNT =====
const activeFiltersCount = useMemo(() => {
let count = 0;
if (tableFilterState.location_id) {
count += 1;
}
if (tableFilterState.project_status) {
count += 1;
}
return count;
}, [tableFilterState.location_id, tableFilterState.project_status]);
const hasFilters = activeFiltersCount > 0;
// ===== SEARCH CHANGE HANDLER =====
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
// ===== HANDLE FILTER MODAL OPEN =====
const handleFilterModalOpen = () => {
filterModal.openModal();
formik.validateForm();
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
// updateFilter('nameSort', '');
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
// updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
}, [sorting]);
return (
<>
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-end items-end sm:items-center gap-4'>
<div className='w-full'>
<div className='flex flex-col mb-4'>
<div className='relative w-full p-3 pt-0 px-0 flex flex-row justify-between gap-3 flex-wrap after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10'>
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput
name='search'
placeholder='Cari Closing'
value={tableFilterState.search}
placeholder='Search'
value={tableFilterState.search ?? ''}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
<div className='grid grid-cols-12 justify-end gap-2'>
<SelectInput
label='Lokasi'
options={locationOptions}
isLoading={isLoadingLocationOptions}
value={selectedLocation}
onChange={locationChangeHandler}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isClearable
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
className={{
wrapper: 'col-span-12 sm:col-span-6',
wrapper: 'w-full min-w-24 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<SelectInput
label='Status Project'
placeholder='Pilih Status'
options={PROJECT_STATUS_OPTIONS}
value={selectedProjectStatus}
onChange={projectStatusChangeHandler}
isClearable
className={{
wrapper: 'col-span-12 sm:col-span-6',
}}
/>
<Button
variant='outline'
color='none'
onClick={handleFilterModalOpen}
className={cn(
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
{
'border-primary-gradient text-primary': hasFilters,
}
)}
>
<Icon icon='heroicons:funnel' width={20} height={20} />
Filter
{hasFilters && (
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
{activeFiltersCount}
</span>
)}
</Button>
</div>
</div>
</div>
<Table<Closing>
data={isResponseSuccess(closings) ? closings?.data : []}
columns={closingsColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={isResponseSuccess(closings) ? closings?.meta?.page : 0}
totalItems={
isResponseSuccess(closings) ? closings?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoadingClosings}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(closings) && closings?.data?.length === 0,
}),
}}
/>
{isLoadingClosings ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
) : data.length === 0 ? (
<ClosingTableSkeleton
columns={closingsColumns}
icon={
<Icon
icon='heroicons:chart-bar'
className='text-white'
width={20}
height={20}
/>
}
title='Data Closing Belum Tersedia'
subtitle='Tidak ada data closing untuk saat ini.'
/>
) : (
<Table<Closing>
data={isResponseSuccess(closings) ? closings?.data : []}
columns={closingsColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={isResponseSuccess(closings) ? closings?.meta?.page : 0}
totalItems={
isResponseSuccess(closings) ? closings?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoadingClosings}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn('mt-3', {
'w-full mb-0':
isResponseSuccess(closings) && closings?.data?.length === 0,
}),
headerColumnClassName: 'text-nowrap',
}}
/>
)}
</div>
</div>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
}}
>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-medium text-sm'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={filterModal.closeModal}
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
<div className='p-4 flex flex-col gap-1.5'>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={locationOptions}
value={locationIdValue}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue(
'location_id',
val?.value ? String(val.value) : null
);
}
}}
onInputChange={setLocationInputValue}
isLoading={isLoadingLocationOptions}
isClearable
onMenuScrollToBottom={loadMoreLocations}
className={{ wrapper: 'w-full' }}
/>
<SelectInputRadio
label='Status Project'
placeholder='Pilih Status'
options={projectStatusOptions}
value={projectStatusValue}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue('project_status', val?.value || null);
}
}}
className={{ wrapper: 'w-full' }}
isClearable={true}
/>
</div>
{/* Modal Footer */}
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button
type='reset'
variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
>
Reset Filter
</Button>
<Button
type='submit'
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
disabled={!formik.isValid || formik.isSubmitting}
>
Apply Filter
</Button>
</div>
</form>
</Modal>
</>
);
};
@@ -0,0 +1,13 @@
import * as yup from 'yup';
export type ClosingFilterType = {
location_id: string | null;
project_status: string | null;
};
export const ClosingFilterSchema = yup.object({
location_id: yup.string().nullable(),
project_status: yup.string().nullable(),
});
export type ClosingFilterValues = yup.InferType<typeof ClosingFilterSchema>;
@@ -1,109 +0,0 @@
'use client';
import React, { useMemo } from 'react';
import { ColumnDef } from '@tanstack/react-table';
import Table from '@/components/Table';
import Card from '@/components/Card';
import { formatCurrency } from '@/lib/helper';
import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing';
interface HppExpeditionReportTableProps {
type?: 'detail';
initialValues?: BaseHppExpedition;
}
const HppExpeditionReportTable = ({
initialValues,
}: HppExpeditionReportTableProps) => {
const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => {
return initialValues?.expedition_costs || [];
}, [initialValues]);
const totals = useMemo(() => {
const totalHpp = initialValues?.total_hpp_amount || 0;
return {
totalHpp,
};
}, [initialValues]);
const costOfRevenueExpeditionColumns: ColumnDef<BaseExpeditionCost>[] =
useMemo(
() => [
{
id: 'id',
accessorKey: 'id',
header: 'No',
cell: (props) => {
return <div>{props.row.index + 1}</div>;
},
footer: () => (
<div className='font-semibold text-gray-900'>
Total HPP Ekspedisi
</div>
),
},
{
id: 'expedition_vendor_name',
accessorKey: 'expedition_vendor_name',
header: 'Nama Ekspedisi',
cell: (props) => props.getValue() || '-',
},
{
id: 'hpp_amount',
accessorKey: 'hpp_amount',
header: 'HPP Ekspedisi',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalHpp)}
</div>
),
},
],
[totals]
);
return (
<>
<section className='w-full'>
<div className='p-4'>
<h2 className='text-xl font-semibold mb-4'>HPP Ekspedisi</h2>
<Card
className={{
wrapper: 'w-full bg-base-100',
body: 'p-0',
}}
>
<Table
data={costOfRevenueExpeditionData}
columns={costOfRevenueExpeditionColumns}
renderFooter={costOfRevenueExpeditionData.length > 0}
className={{
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
/>
</Card>
</div>
</section>
</>
);
};
export default HppExpeditionReportTable;
@@ -0,0 +1,36 @@
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
import Table from '@/components/Table';
import { ColumnDef } from '@tanstack/react-table';
const ClosingTabSkeleton = <T extends object>({
columns,
icon,
title,
subtitle,
}: {
columns: ColumnDef<T, unknown>[];
icon: React.ReactNode;
title: string;
subtitle: string;
}) => {
return (
<div className='relative size-full'>
<Table
data={[]}
columns={columns}
isLoading={true}
className={{
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
headerColumnClassName: 'whitespace-nowrap',
containerClassName: 'mb-0 overflow-hidden',
tableWrapperClassName: 'overflow-hidden',
}}
/>
<div className='absolute inset-0 flex items-center justify-center'>
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
</div>
</div>
);
};
export default ClosingTabSkeleton;
@@ -0,0 +1,37 @@
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
import Table from '@/components/Table';
import { Closing } from '@/types/api/closing';
import { ColumnDef } from '@tanstack/react-table';
const ClosingTableSkeleton = ({
columns,
icon,
title,
subtitle,
}: {
columns: ColumnDef<Closing>[];
icon: React.ReactNode;
title: string;
subtitle: string;
}) => {
return (
<div className='relative size-full'>
<Table
data={[]}
columns={columns}
isLoading={true}
className={{
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
headerColumnClassName: 'whitespace-nowrap',
containerClassName: 'mb-0 overflow-hidden',
tableWrapperClassName: 'overflow-hidden',
}}
/>
<div className='absolute inset-0 flex items-center justify-center'>
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
</div>
</div>
);
};
export default ClosingTableSkeleton;
@@ -0,0 +1,40 @@
import { Icon } from '@iconify/react';
import Card from '@/components/Card';
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
const FinanceClosingSkeleton = ({
title = 'Data Keuangan Belum Tersedia',
subtitle = 'Tidak ada data keuangan untuk periode ini.',
iconName = 'heroicons:chart-bar',
}: {
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<Card
variant='bordered'
className={{
wrapper: 'w-full',
body: 'p-8',
}}
>
<div className='flex items-center justify-center p-8'>
<DataStateSkeleton
icon={
<Icon
icon={iconName}
className='text-white'
width={20}
height={20}
/>
}
title={title}
description={subtitle}
/>
</div>
</Card>
);
};
export default FinanceClosingSkeleton;
@@ -0,0 +1,29 @@
import { Icon } from '@iconify/react';
import ClosingTabSkeleton from './ClosingTabSkeleton';
import { BaseExpeditionCost } from '@/types/api/closing';
import { ColumnDef } from '@tanstack/react-table';
const HppExpeditionClosingSkeleton = ({
columns,
title = 'Data HPP Ekspedisi Belum Tersedia',
subtitle = 'Tidak ada data HPP ekspedisi untuk periode ini.',
iconName = 'heroicons:chart-bar',
}: {
columns: ColumnDef<BaseExpeditionCost>[];
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<ClosingTabSkeleton<BaseExpeditionCost>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title}
subtitle={subtitle}
/>
);
};
export default HppExpeditionClosingSkeleton;
@@ -0,0 +1,29 @@
import { Icon } from '@iconify/react';
import ClosingTabSkeleton from './ClosingTabSkeleton';
import { Overhead } from '@/types/api/closing';
import { ColumnDef } from '@tanstack/react-table';
const OverheadClosingSkeleton = ({
columns,
title = 'Data Overhead Belum Tersedia',
subtitle = 'Tidak ada data overhead untuk periode ini.',
iconName = 'heroicons:chart-bar',
}: {
columns: ColumnDef<Overhead>[];
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<ClosingTabSkeleton<Overhead>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title}
subtitle={subtitle}
/>
);
};
export default OverheadClosingSkeleton;
@@ -0,0 +1,33 @@
import { Icon } from '@iconify/react';
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
const ProductionDataClosingSkeleton = ({
title = 'Data Produksi Belum Tersedia',
subtitle = 'Tidak ada data produksi untuk periode ini.',
iconName = 'heroicons:chart-bar',
}: {
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<div className='w-full rounded-xl p-8 shadow-sm'>
<div className='flex items-center justify-center p-12'>
<DataStateSkeleton
icon={
<Icon
icon={iconName}
className='text-white'
width={20}
height={20}
/>
}
title={title}
description={subtitle}
/>
</div>
</div>
);
};
export default ProductionDataClosingSkeleton;
@@ -0,0 +1,29 @@
import { Icon } from '@iconify/react';
import ClosingTabSkeleton from './ClosingTabSkeleton';
import { BaseSales } from '@/types/api/closing';
import { ColumnDef } from '@tanstack/react-table';
const SalesClosingSkeleton = ({
columns,
title = 'Data Penjualan Belum Tersedia',
subtitle = 'Tidak ada data penjualan untuk periode ini.',
iconName = 'heroicons:chart-bar',
}: {
columns: ColumnDef<BaseSales>[];
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<ClosingTabSkeleton<BaseSales>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title}
subtitle={subtitle}
/>
);
};
export default SalesClosingSkeleton;
@@ -0,0 +1,29 @@
import { Icon } from '@iconify/react';
import ClosingTabSkeleton from './ClosingTabSkeleton';
import { RowSapronakCalculation } from '@/types/api/closing';
import { ColumnDef } from '@tanstack/react-table';
const SapronakCalculationClosingSkeleton = ({
columns,
title = 'Data Perhitungan Sapronak Belum Tersedia',
subtitle = 'Tidak ada data perhitungan sapronak untuk periode ini.',
iconName = 'heroicons:chart-bar',
}: {
columns: ColumnDef<RowSapronakCalculation>[];
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<ClosingTabSkeleton<RowSapronakCalculation>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title}
subtitle={subtitle}
/>
);
};
export default SapronakCalculationClosingSkeleton;
@@ -0,0 +1,40 @@
import { Icon } from '@iconify/react';
import ClosingTabSkeleton from './ClosingTabSkeleton';
import { ColumnDef } from '@tanstack/react-table';
const SapronakClosingSkeleton = <T extends object>({
columns,
type = 'incoming',
title,
subtitle,
iconName = 'heroicons:chart-bar',
}: {
columns: ColumnDef<T, unknown>[];
type?: 'incoming' | 'outgoing';
title?: string;
subtitle?: string;
iconName?: string;
}) => {
const defaultTitle =
type === 'incoming'
? 'Data Sapronak Masuk Belum Tersedia'
: 'Data Sapronak Keluar Belum Tersedia';
const defaultSubtitle =
type === 'incoming'
? 'Tidak ada data sapronak masuk untuk periode ini.'
: 'Tidak ada data sapronak keluar untuk periode ini.';
return (
<ClosingTabSkeleton<T>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title || defaultTitle}
subtitle={subtitle || defaultSubtitle}
/>
);
};
export default SapronakClosingSkeleton;
@@ -0,0 +1,13 @@
import FinanceClosingTable from '@/components/pages/closing/table/FinanceClosingTable';
const FinanceClosingTab = ({ projectFlockId }: { projectFlockId: number }) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<FinanceClosingTable projectFlockId={projectFlockId} />
)}
</div>
);
};
export default FinanceClosingTab;
@@ -0,0 +1,19 @@
import HppExpeditionClosingTable from '@/components/pages/closing/table/HppExpeditionClosingTable';
interface HppExpeditionClosingTabProps {
projectFlockId: number;
}
const HppExpeditionClosingTab = ({
projectFlockId,
}: HppExpeditionClosingTabProps) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<HppExpeditionClosingTable projectFlockId={projectFlockId} />
)}
</div>
);
};
export default HppExpeditionClosingTab;
@@ -1,22 +1,22 @@
import ClosingOverheadTable from '@/components/pages/closing/ClosingOverheadTable';
import OverheadClosingTable from '@/components/pages/closing/table/OverheadClosingTable';
import { ClosingGeneralInformation } from '@/types/api/closing';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
interface ClosingOverheadTabContentProps {
interface OverheadClosingTabProps {
projectFlockId: number;
generalInformation?: ClosingGeneralInformation;
kandangData?: ProjectFlockKandang;
}
const ClosingOverheadTabContent = ({
const OverheadClosingTab = ({
projectFlockId,
generalInformation,
kandangData,
}: ClosingOverheadTabContentProps) => {
}: OverheadClosingTabProps) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<ClosingOverheadTable
<OverheadClosingTable
projectFlockId={projectFlockId}
generalInformation={generalInformation}
kandangData={kandangData}
@@ -26,4 +26,4 @@ const ClosingOverheadTabContent = ({
);
};
export default ClosingOverheadTabContent;
export default OverheadClosingTab;
@@ -0,0 +1,321 @@
'use client';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { ClosingApi } from '@/services/api/closing';
import { isResponseSuccess } from '@/lib/api-helper';
import { formatNumber } from '@/lib/helper';
import ProductionDataClosingSkeleton from '@/components/pages/closing/skeleton/ProductionDataClosingSkeleton';
import Card from '@/components/Card';
interface ProductionDataClosingTabProps {
projectFlockId: number;
}
const ProductionDataClosingTab = ({
projectFlockId,
}: ProductionDataClosingTabProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: productionData, isLoading } = useSWR(
`${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
() => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
);
if (isLoading) {
return <ProductionDataClosingSkeleton />;
}
if (!productionData || !isResponseSuccess(productionData)) {
return (
<ProductionDataClosingSkeleton
iconName='heroicons:exclamation-circle'
title='Gagal Memuat Data Produksi'
subtitle='Terjadi kesalahan saat memuat data produksi. Silakan coba lagi.'
/>
);
}
const { purchase, sales, performance } = productionData.data;
// Helper for consistent row styling
const DataRow = ({
label,
value,
unit = '',
valueClassName = 'font-bold text-gray-800',
unitClassName = 'text-gray-500 w-12 text-right',
}: {
label: string;
value: string | number;
unit?: string;
valueClassName?: string;
unitClassName?: string;
}) => (
<div className='flex justify-between items-center py-1'>
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
<div className='flex gap-2 w-1/2 justify-end items-center'>
<span className={valueClassName}>{value}</span>
{unit && <span className={unitClassName}>{unit}</span>}
</div>
</div>
);
return (
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Data Produksi'
collapsible
defaultCollapsed={false}
>
<div className='p-6'>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
{/* Left Column */}
<div className='space-y-10'>
{/* Purchase Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Pembelian
</h3>
<div className='space-y-1'>
<DataRow
label='Populasi Awal'
value={formatNumber(purchase.initial_population)}
unit='Ekor'
/>
<DataRow
label='Claim Culling'
value={formatNumber(purchase.claim_culling)}
unit='Ekor'
/>
<DataRow
label='Populasi Akhir'
value={formatNumber(purchase.final_population)}
unit='Ekor'
/>
<DataRow
label='Pakan Masuk'
value={formatNumber(purchase.feed_in)}
unit='Kg'
/>
<DataRow
label='Pakan Terpakai'
value={formatNumber(purchase.feed_used)}
unit='Kg'
/>
</div>
</section>
{/* Sales Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Penjualan
</h3>
<div className='space-y-4'>
{/* Chicken Sales */}
<div className='space-y-1'>
<DataRow
label='Penjualan (Ekor)'
value={formatNumber(sales.chicken.sales_population)}
unit='Ekor'
/>
<DataRow
label='Penjualan (Kg)'
value={formatNumber(sales.chicken.sales_weight)}
unit='Kg'
/>
<DataRow
label='Bobot Rata-Rata'
value={formatNumber(sales.chicken.avg_weight)}
unit='Kg/Ekor'
/>
<DataRow
label='Harga Jual Rata-Rata'
value={formatNumber(sales.chicken.avg_selling_price)}
unit='Rupiah'
/>
</div>
{/* Egg Sales (if available) */}
{sales.egg && (
<>
<div className='h-px bg-gray-100 my-2' />
<div className='space-y-1'>
<DataRow
label='Telur (Butir)'
value={formatNumber(sales.egg.egg_pieces)}
unit='Butir'
/>
<DataRow
label='Telur (Kg)'
value={formatNumber(sales.egg.egg_mass)}
unit='Kg'
/>
<DataRow
label='Berat Telur Rata-Rata'
value={formatNumber(sales.egg.avg_egg_weight)}
unit='Kg'
/>
<DataRow
label='Harga Jual Telur Rata-Rata'
value={formatNumber(sales.egg.avg_selling_price)}
unit='Rupiah'
/>
</div>
</>
)}
</div>
</section>
</div>
{/* Divider Line (Absolute centered) */}
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
{/* Right Column */}
<div className='space-y-10 flex flex-col h-full'>
{/* Performance Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Performance
</h3>
<div className='space-y-1'>
<DataRow
label='Deplesi'
value={formatNumber(performance.depletion)}
unit='Ekor'
/>
<DataRow
label='Umur'
value={formatNumber(performance.age_day)}
unit='Hari'
/>
<DataRow
label='Mortalitas Std'
value={formatNumber(performance.mor_std)}
unitClassName='hidden'
/>
<DataRow
label='Mortalitas Act'
value={formatNumber(performance.mor_act)}
unitClassName='hidden'
/>
<DataRow
label='DEFF Mortalitas'
value={formatNumber(performance.mor_diff)}
unitClassName='hidden'
/>
{/* <DataRow
label='AWG Std'
value={formatNumber(performance.awg_std)}
unit='Gr/Hari'
/>
<DataRow
label='AWG Act'
value={formatNumber(performance.awg_act)}
unit='Gr/Hari'
/> */}
<DataRow
label='Feed Intake Std'
value={formatNumber(performance.feed_intake_std)}
unitClassName='hidden'
/>
<DataRow
label='Feed Intake Act'
value={formatNumber(performance.feed_intake)}
unitClassName='hidden'
/>
<DataRow
label='FCR Std'
value={formatNumber(performance.fcr_std)}
unitClassName='hidden'
/>
<DataRow
label='FCR Act'
value={formatNumber(performance.fcr_act)}
unitClassName='hidden'
/>
<DataRow
label='DEFF FCR'
value={formatNumber(performance.fcr_diff)}
unitClassName='hidden'
/>
{/* Laying Specific Fields */}
{performance.hen_day_act !== undefined && (
<>
<DataRow
label='Hen Day Std'
value={formatNumber(performance.hen_day_std!)}
unit='%'
/>
<DataRow
label='Hen Day Act'
value={formatNumber(performance.hen_day_act)}
unit='%'
/>
</>
)}
{performance.egg_mass !== undefined && (
<>
<DataRow
label='Egg Mass Std'
value={formatNumber(performance.egg_mass_std!)}
unit='Kg'
/>
<DataRow
label='Egg Mass Act'
value={formatNumber(performance.egg_mass)}
unit='Kg'
/>
</>
)}
{performance.egg_weight !== undefined && (
<>
<DataRow
label='Egg Weight Std'
value={formatNumber(performance.egg_weight_std!)}
unit='Gr'
/>
<DataRow
label='Egg Weight Act'
value={formatNumber(performance.egg_weight)}
unit='Gr'
/>
</>
)}
{performance.hen_housed_act !== undefined && (
<>
<DataRow
label='Hen Housed Std'
value={formatNumber(performance.hen_housed_std!)}
unit='%'
/>
<DataRow
label='Hen Housed Act'
value={formatNumber(performance.hen_housed_act)}
unit='%'
/>
</>
)}
</div>
</section>
</div>
</div>
</div>
</Card>
</div>
);
};
export default ProductionDataClosingTab;
@@ -0,0 +1,15 @@
import SalesClosingTable from '@/components/pages/closing/table/SalesClosingTable';
interface SalesClosingTabProps {
projectFlockId: number;
}
const SalesClosingTab = ({ projectFlockId }: SalesClosingTabProps) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && <SalesClosingTable projectFlockId={projectFlockId} />}
</div>
);
};
export default SalesClosingTab;
@@ -1,22 +1,22 @@
'use client';
import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable';
import SapronakCalculationClosingTable from '@/components/pages/closing/table/SapronakCalculationClosingTable';
import { ClosingGeneralInformation } from '@/types/api/closing';
interface ClosingSapronakCalculationTabContentProps {
interface SapronakCalculationClosingTabProps {
projectFlockId?: number;
closingGeneralInformation?: ClosingGeneralInformation;
}
const ClosingSapronakCalculationTabContent = ({
const SapronakCalculationClosingTab = ({
projectFlockId,
closingGeneralInformation,
}: ClosingSapronakCalculationTabContentProps) => {
}: SapronakCalculationClosingTabProps) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<>
<ClosingSapronakCalculationTable
<SapronakCalculationClosingTable
closingGeneralInformation={closingGeneralInformation}
projectFlockId={projectFlockId}
/>
@@ -26,4 +26,4 @@ const ClosingSapronakCalculationTabContent = ({
);
};
export default ClosingSapronakCalculationTabContent;
export default SapronakCalculationClosingTab;
@@ -0,0 +1,30 @@
'use client';
import IncomingSapronaksTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksTable';
import OutgoingSapronaksTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksTable';
import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable';
import OutgoingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable';
interface SapronakClosingTabProps {
projectFlockId?: number;
}
const SapronakClosingTab = ({ projectFlockId }: SapronakClosingTabProps) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<>
<IncomingSapronaksTable projectFlockId={projectFlockId} />
<IncomingSapronaksSummaryTable projectFlockId={projectFlockId} />
<OutgoingSapronaksTable projectFlockId={projectFlockId} />
<OutgoingSapronaksSummaryTable projectFlockId={projectFlockId} />
</>
)}
</div>
);
};
export default SapronakClosingTab;
@@ -0,0 +1,507 @@
import Alert from '@/components/Alert';
import Card from '@/components/Card';
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { formatCurrency, formatTitleCase } from '@/lib/helper';
import { ClosingApi } from '@/services/api/closing';
import { HppItem, ProfitLossItem } from '@/types/api/closing';
import { Icon } from '@iconify/react';
import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import useSWR from 'swr';
import FinanceClosingSkeleton from '@/components/pages/closing/skeleton/FinanceClosingSkeleton';
const FinanceClosingTable = ({
projectFlockId,
}: {
projectFlockId: number;
}) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: finance, isLoading } = useSWR(
`/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
() =>
ClosingApi.getFinance(
projectFlockId,
kandangId ? Number(kandangId) : undefined
)
);
const hppTableData: HppItem[] = useMemo(() => {
if (isResponseSuccess(finance)) {
const customItems = {
label: 'HPP dan Pengeluaran',
code: 'custom_row',
} as HppItem;
const purchases = finance.data.hpp.items.filter(
(item) => item.category === 'purchase'
);
const totalBudgeting = {
label: 'HPP dan Bahan Baku',
code: 'custom_row',
} as HppItem;
const overheads = finance.data.hpp.items.filter(
(item) => item.category === 'overhead'
);
return [customItems, ...purchases, totalBudgeting, ...overheads];
}
return [];
}, [finance]);
const profitLossTableData: ProfitLossItem[] = useMemo(() => {
if (isResponseSuccess(finance)) {
const incomes = finance.data.profit_loss.items.filter(
(item) => item.type === 'income'
);
const purchases = finance.data.profit_loss.items.filter(
(item) => item.type === 'purchase'
);
const overheads = finance.data.profit_loss.items.filter(
(item) => item.type === 'overhead'
);
const grossProfit = {
label: 'LABA RUGI BRUTO',
code: 'custom_row',
type: 'gross_profit',
rp_per_bird:
finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
} as ProfitLossItem;
const subtotal = {
label: 'Subtotal',
code: 'custom_row',
type: 'subtotal',
rp_per_bird:
finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
} as ProfitLossItem;
return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
}
return [];
}, [finance]);
return (
<div className='flex flex-col gap-4 pt-3'>
{isLoading ? (
<FinanceClosingSkeleton />
) : !isResponseSuccess(finance) ? (
<FinanceClosingSkeleton
iconName='heroicons:chart-bar'
title='Data Keuangan Tidak Ditemukan'
subtitle='Tidak ada data keuangan untuk periode ini.'
/>
) : (
<>
<section className='grid grid-cols-1 md:grid-cols-2 gap-3'>
<Card
className={{
wrapper: 'w-full rounded-xl border border-base-content/10',
body: 'p-0',
wrapperContent:
'h-full flex flex-col items-between justify-between',
}}
variant='bordered'
>
<div className='flex flex-row items-center gap-4 px-4 py-4'>
<Alert
variant='soft'
color='success'
className='rounded-lg p-3 bg-success/12 flex items-center justify-center'
>
<Icon
icon='heroicons:chart-bar-square'
width={24}
height={24}
/>
</Alert>
<div className='space-y-1'>
<h3 className='text-base-content/50 font-semibold text-sm'>
Laba Rugi Brutto
</h3>
<p className='text-xl font-semibold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.gross_profit.amount
)
: '-'}
</p>
</div>
</div>
</Card>
<Card
className={{
wrapper: 'w-full rounded-xl border border-base-content/10',
body: 'p-0',
wrapperContent:
'h-full flex flex-col items-between justify-between',
}}
variant='bordered'
>
<div className='flex flex-row items-center gap-4 px-4 py-4'>
<Alert
variant='soft'
color='info'
className='rounded-lg p-3 bg-info/12 flex items-center justify-center'
>
<Icon
icon='heroicons:currency-dollar'
width={24}
height={24}
/>
</Alert>
<div className='space-y-1'>
<h3 className='text-base-content/50 font-semibold text-sm'>
Laba Rugi Netto
</h3>
<p className='text-xl font-semibold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit.amount
)
: '-'}
</p>
</div>
</div>
</Card>
</section>
<Card
title='HPP Purchases'
variant='bordered'
collapsible
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
>
<div className='p-0'>
<Table<HppItem>
data={hppTableData}
isLoading={isLoading}
columns={[
{
header: 'No.',
enableSorting: false,
accessorFn: (item, index) => {
if (item.code === 'custom_row') return '-';
const dataRowsBefore = hppTableData
.slice(0, index)
.filter((row) => row.code !== 'custom_row').length;
return dataRowsBefore + 1;
},
footer: () => {
return 'HPP';
},
},
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => formatTitleCase(item.label || '-'),
},
{
header: 'Budgeting',
enableSorting: false,
columns: [
{
header: 'Rp/Ekor',
id: 'budgeting_rp_per_bird',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.rp_per_bird || 0),
footer: (props) => {
return props.column.id === 'budgeting_rp_per_bird' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting
?.rp_per_bird || 0
)
: '-';
},
},
{
header: 'Rp/Kg',
id: 'budgeting_rp_per_kg',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.rp_per_kg || 0),
footer: (props) => {
return props.column.id === 'budgeting_rp_per_kg' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting
?.rp_per_kg || 0
)
: '-';
},
},
{
header: 'Jumlah (Rp)',
id: 'budgeting_amount',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.amount || 0),
footer: (props) => {
return props.column.id === 'budgeting_amount' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting?.amount || 0
)
: '-';
},
},
],
},
{
header: 'Realization',
enableSorting: false,
columns: [
{
header: 'Rp/Ekor',
id: 'realization_rp_per_bird',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.rp_per_bird || 0),
footer: (props) => {
return props.column.id ===
'realization_rp_per_bird' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization
?.rp_per_bird || 0
)
: '-';
},
},
{
header: 'Rp/Kg',
id: 'realization_rp_per_kg',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.rp_per_kg || 0),
footer: (props) => {
return props.column.id === 'realization_rp_per_kg' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization
?.rp_per_kg || 0
)
: '-';
},
},
{
header: 'Jumlah (Rp)',
id: 'realization_amount',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.amount || 0),
footer: (props) => {
return props.column.id === 'realization_amount' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization?.amount ||
0
)
: '-';
},
},
],
},
]}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
paginationClassName: 'hidden',
}}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.code === 'custom_row') {
return (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
></td>
<td
colSpan={7}
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
</tr>
);
}
return null;
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
<Card
title='Profit/Loss'
variant='bordered'
collapsible
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
>
<div className='p-0'>
<Table<ProfitLossItem>
data={profitLossTableData}
isLoading={isLoading}
columns={[
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => item.label,
cell: (item) => (
<div className=''>
{formatTitleCase(item.row.original.label || '-')}
</div>
),
footer: () => (
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
),
},
{
header: 'Rp/Ekor',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_bird || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Rp/Kg',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_kg || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Jumlah (Rp)',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.amount || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.amount || 0
)
: formatCurrency(0)}
</div>
),
},
]}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
paginationClassName: 'hidden',
}}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.code === 'custom_row') {
return (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.footerRowClassName}
>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold ps-6 uppercase'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_bird ?? 0)}
</div>
</td>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_kg ?? 0)}
</div>
</td>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatCurrency(rowData.amount ?? 0)}
</div>
</td>
</tr>
);
}
return null;
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
</>
)}
</div>
);
};
export default FinanceClosingTable;
@@ -0,0 +1,147 @@
'use client';
import React, { useMemo } from 'react';
import { ColumnDef } from '@tanstack/react-table';
import Table from '@/components/Table';
import Card from '@/components/Card';
import { formatCurrency } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseExpeditionCost } from '@/types/api/closing';
import { ClosingApi } from '@/services/api/closing';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import HppExpeditionClosingSkeleton from '@/components/pages/closing/skeleton/HppExpeditionClosingSkeleton';
interface HppExpeditionClosingTableProps {
projectFlockId: number;
}
const HppExpeditionClosingTable = ({
projectFlockId,
}: HppExpeditionClosingTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: hppExpedition, isLoading } = useSWR(
kandangId
? `/closing/hpp-expedition/${projectFlockId}/${kandangId}`
: `/closing/hpp-expedition/${projectFlockId}`,
() =>
kandangId
? ClosingApi.getHppEkspedisiByKandang(projectFlockId, Number(kandangId))
: ClosingApi.getHppEkspedisi(projectFlockId)
);
const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => {
if (isResponseSuccess(hppExpedition)) {
return hppExpedition.data.expedition_costs || [];
}
return [];
}, [hppExpedition]);
const totals = useMemo(() => {
if (isResponseSuccess(hppExpedition)) {
return {
totalHpp: hppExpedition.data.total_hpp_amount || 0,
};
}
return {
totalHpp: 0,
};
}, [hppExpedition]);
const costOfRevenueExpeditionColumns: ColumnDef<BaseExpeditionCost>[] =
useMemo(
() => [
{
id: 'id',
accessorKey: 'id',
header: 'No',
cell: (props) => {
return <div>{props.row.index + 1}</div>;
},
footer: () => (
<div className='font-semibold text-gray-900'>
Total HPP Ekspedisi
</div>
),
},
{
id: 'expedition_vendor_name',
accessorKey: 'expedition_vendor_name',
header: 'Nama Ekspedisi',
cell: (props) => props.getValue() || '-',
},
{
id: 'hpp_amount',
accessorKey: 'hpp_amount',
header: 'HPP Ekspedisi',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalHpp)}
</div>
),
},
],
[totals]
);
return (
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='HPP Ekspedisi'
collapsible
defaultCollapsed={false}
>
{isLoading ? (
<HppExpeditionClosingSkeleton
columns={costOfRevenueExpeditionColumns}
/>
) : costOfRevenueExpeditionData.length === 0 ? (
<HppExpeditionClosingSkeleton
columns={costOfRevenueExpeditionColumns}
iconName='heroicons:chart-bar'
/>
) : (
<Table
data={costOfRevenueExpeditionData}
columns={costOfRevenueExpeditionColumns}
isLoading={isLoading}
renderFooter={costOfRevenueExpeditionData.length > 0}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
/>
)}
</Card>
</div>
);
};
export default HppExpeditionClosingTable;
@@ -1,5 +1,5 @@
import Card from '@/components/Card';
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { ClosingApi } from '@/services/api/closing';
@@ -14,18 +14,19 @@ import { ColumnDef } from '@tanstack/react-table';
import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import useSWR from 'swr';
import OverheadClosingSkeleton from '@/components/pages/closing/skeleton/OverheadClosingSkeleton';
interface ClosingOverheadTableProps {
interface OverheadClosingTableProps {
projectFlockId: number;
generalInformation?: ClosingGeneralInformation;
kandangData?: ProjectFlockKandang;
}
const ClosingOverheadTable = ({
const OverheadClosingTable = ({
projectFlockId,
generalInformation,
kandangData,
}: ClosingOverheadTableProps) => {
}: OverheadClosingTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
@@ -37,7 +38,7 @@ const ClosingOverheadTable = ({
}
);
const { data: overheadKandang, isLoading: isLoadingOverheadKandang } = useSWR(
const { data: overheadKandang } = useSWR(
kandangId
? `${ClosingApi.basePath}/${projectFlockId}/${kandangId}/overhead`
: undefined,
@@ -208,42 +209,84 @@ const ClosingOverheadTable = ({
);
return (
<>
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Pengeluaran Overhead'
collapsible
defaultCollapsed={false}
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Table<Overhead>
data={
kandangId
? isResponseSuccess(overheadKandang)
? (overheadKandang.data?.overheads ?? [])
: []
: isResponseSuccess(overhead)
? (overhead.data?.overheads ?? [])
: []
}
columns={columns}
className={{
containerClassName: 'my-4',
headerColumnClassName: cn(
TABLE_DEFAULT_STYLING.headerColumnClassName,
'whitespace-nowrap'
),
}}
isLoading={isLoadingOverhead}
renderFooter={
isResponseSuccess(overhead)
? overhead.data?.overheads.length > 0
: false
}
/>
{kandangId && (
{isLoadingOverhead ? (
<OverheadClosingSkeleton columns={columns} />
) : !isResponseSuccess(overhead) ? (
<OverheadClosingSkeleton
columns={columns}
iconName='heroicons:chart-bar'
title='Data Overhead Tidak Ditemukan'
subtitle='Tidak ada data overhead untuk periode ini.'
/>
) : kandangId && !isResponseSuccess(overheadKandang) ? (
<OverheadClosingSkeleton
columns={columns}
iconName='heroicons:chart-bar'
title='Data Overhead Tidak Ditemukan'
subtitle='Tidak ada data overhead untuk periode ini.'
/>
) : (!kandangId && overhead.data?.overheads.length === 0) ||
(kandangId &&
isResponseSuccess(overheadKandang) &&
overheadKandang.data?.overheads.length === 0) ? (
<OverheadClosingSkeleton
columns={columns}
iconName='heroicons:chart-bar'
/>
) : (
<Table<Overhead>
data={
kandangId
? isResponseSuccess(overheadKandang)
? (overheadKandang.data?.overheads ?? [])
: []
: isResponseSuccess(overhead)
? (overhead.data?.overheads ?? [])
: []
}
columns={columns}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName: cn(
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
'whitespace-nowrap'
),
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
isLoading={isLoadingOverhead}
renderFooter={
isResponseSuccess(overhead)
? overhead.data?.overheads.length > 0
: false
}
/>
)}
{kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && (
<Card
className={{
wrapper: 'w-full',
@@ -298,8 +341,8 @@ const ClosingOverheadTable = ({
</Card>
)}
</Card>
</>
</div>
);
};
export default ClosingOverheadTable;
export default OverheadClosingTable;
@@ -5,28 +5,47 @@ import { ColumnDef } from '@tanstack/react-table';
import Table from '@/components/Table';
import Card from '@/components/Card';
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
import {
BaseClosingSales,
BaseSales,
ClosingSalesSummary,
} from '@/types/api/closing';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseSales, ClosingSalesSummary } from '@/types/api/closing';
import { Product } from '@/types/api/master-data/product';
import { Customer } from '@/types/api/master-data/customer';
import { Kandang } from '@/types/api/master-data/kandang';
import { ClosingApi } from '@/services/api/closing';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import SalesClosingSkeleton from '@/components/pages/closing/skeleton/SalesClosingSkeleton';
interface SalesReportTableProps {
type?: 'detail';
initialValues?: BaseClosingSales;
interface SalesClosingTableProps {
projectFlockId: number;
}
const SalesReportTable = ({ initialValues }: SalesReportTableProps) => {
const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: sales, isLoading } = useSWR(
kandangId
? `/closing/sales/${projectFlockId}/${kandangId}`
: `/closing/sales/${projectFlockId}`,
() =>
kandangId
? ClosingApi.getPenjualanByKandang(projectFlockId, Number(kandangId))
: ClosingApi.getPenjualan(projectFlockId)
);
const salesData: BaseSales[] = useMemo(() => {
return initialValues?.sales || [];
}, [initialValues]);
if (isResponseSuccess(sales)) {
return sales.data.sales || [];
}
return [];
}, [sales]);
const summary: ClosingSalesSummary | undefined = useMemo(() => {
return initialValues?.summary;
}, [initialValues]);
if (isResponseSuccess(sales)) {
return sales.data.summary;
}
return undefined;
}, [sales]);
const totals = useMemo(() => {
if (salesData.length === 0) {
@@ -293,41 +312,55 @@ const SalesReportTable = ({ initialValues }: SalesReportTableProps) => {
);
return (
<>
<section className='w-full'>
<div className='p-4'>
<h2 className='text-xl font-semibold mb-4'>Penjualan</h2>
<Card
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Penjualan'
collapsible
defaultCollapsed={false}
>
{isLoading ? (
<SalesClosingSkeleton columns={salesColumns} />
) : salesData.length === 0 ? (
<SalesClosingSkeleton
columns={salesColumns}
iconName='heroicons:chart-bar'
/>
) : (
<Table
data={salesData}
columns={salesColumns}
isLoading={isLoading}
renderFooter={salesData.length > 0}
className={{
wrapper: 'w-full bg-base-100',
body: 'p-0',
containerClassName: 'w-full mb-0!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
>
<Table
data={salesData}
columns={salesColumns}
renderFooter={salesData.length > 0}
className={{
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
/>
</Card>
</div>
</section>
</>
/>
)}
</Card>
</div>
);
};
export default SalesReportTable;
export default SalesClosingTable;
@@ -0,0 +1,359 @@
'use client';
import Card from '@/components/Card';
import Table from '@/components/Table';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import {
RowSapronakCalculation,
TotalSapronakCalculation,
} from '@/types/api/closing';
import { ColumnDef } from '@tanstack/react-table';
import { useMemo } from 'react';
import useSWR from 'swr';
import { ClosingApi } from '@/services/api/closing';
import { isResponseSuccess } from '@/lib/api-helper';
import { ClosingGeneralInformation } from '@/types/api/closing';
import { useSearchParams } from 'next/navigation';
import SapronakCalculationClosingSkeleton from '@/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton';
interface SapronakCalculationClosingTableProps {
projectFlockId: number;
closingGeneralInformation?: ClosingGeneralInformation;
}
const SapronakCalculationClosingTable = ({
projectFlockId,
closingGeneralInformation,
}: SapronakCalculationClosingTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: sapronakCalculation, isLoading } = useSWR(
`/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
() => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)),
{
keepPreviousData: true,
}
);
// Helper function to create columns with footer support
const createColumns = (
total?: TotalSapronakCalculation
): ColumnDef<RowSapronakCalculation>[] => [
{
header: 'Tanggal',
accessorKey: 'date',
cell: (props) =>
props.row.original.date
? formatDate(props.row.original.date, 'DD MMM YYYY')
: '-',
footer: 'Total',
},
{
header: 'No. Referensi',
accessorKey: 'reference_number',
cell: (props) => (props.row.original.reference_number as string) || '-',
footer: '',
},
{
header: 'QTY Masuk',
accessorKey: 'qty_in',
cell: (props) =>
props.row.original.qty_in
? formatNumber(props.row.original.qty_in as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_in ? formatNumber(total?.qty_in) : '0'}
</div>
)
: '',
},
{
header: 'QTY Keluar',
accessorKey: 'qty_out',
cell: (props) =>
props.row.original.qty_out
? formatNumber(props.row.original.qty_out as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_out ? formatNumber(total?.qty_out) : '0'}
</div>
)
: '',
},
{
header: 'QTY Pakai',
accessorKey: 'qty_used',
cell: (props) =>
props.row.original.qty_used
? formatNumber(props.row.original.qty_used as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_used ? formatNumber(total?.qty_used) : '0'}
</div>
)
: '',
},
{
header: 'Uraian',
accessorKey: 'description',
cell: (props) => (props.row.original.description as string) || '-',
footer: '',
},
{
header: 'Kategori Produk',
accessorKey: 'product_category',
cell: (props) => (props.row.original.product_category as string) || '-',
footer: '',
},
{
header: 'Harga Beli/Qty (Rp)',
accessorKey: 'unit_price',
cell: (props) =>
props.row.original.unit_price
? formatCurrency(props.row.original.unit_price as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.avg_unit_price
? formatCurrency(total?.avg_unit_price)
: '-'}
</div>
)
: '',
},
{
header: 'Total Harga (Rp)',
accessorKey: 'total_amount',
cell: (props) =>
props.row.original.total_amount
? formatCurrency(props.row.original.total_amount as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.total_amount ? formatCurrency(total?.total_amount) : '-'}
</div>
)
: '',
},
{
header: 'Keterangan',
accessorKey: 'notes',
cell: (props) => (props.row.original.notes as string) || '-',
footer: '',
},
];
// Memoize columns untuk setiap kategori
const docColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.doc?.total)
: createColumns(),
[sapronakCalculation]
);
const ovkColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.ovk?.total)
: createColumns(),
[sapronakCalculation]
);
const pakanColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.pakan?.total)
: createColumns(),
[sapronakCalculation]
);
return (
<div className='flex flex-col gap-4 pt-3'>
{/* Table DOC jika kategori Project Flock Growing */}
<Card
title={
closingGeneralInformation?.project_type == 'GROWING'
? 'DOC'
: 'Pullet'
}
collapsible
defaultCollapsed={false}
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
>
{isLoading ? (
<SapronakCalculationClosingSkeleton columns={docColumns} />
) : isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.doc?.rows?.length === 0 ? (
<SapronakCalculationClosingSkeleton
columns={docColumns}
iconName='heroicons:chart-bar'
title='Data Perhitungan Sapronak Tidak Ditemukan'
subtitle='Tidak ada data perhitungan sapronak untuk periode ini.'
/>
) : (
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.doc?.rows ?? [])
: []
}
columns={docColumns}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
renderFooter={
isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.doc?.rows?.length > 0
}
/>
)}
</Card>
<Card
title='OVK'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
>
{isLoading ? (
<SapronakCalculationClosingSkeleton columns={ovkColumns} />
) : isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.ovk?.rows?.length === 0 ? (
<SapronakCalculationClosingSkeleton
columns={ovkColumns}
iconName='heroicons:chart-bar'
title='Data Perhitungan Sapronak Tidak Ditemukan'
subtitle='Tidak ada data perhitungan sapronak untuk periode ini.'
/>
) : (
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.ovk?.rows ?? [])
: []
}
columns={ovkColumns}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
renderFooter={
isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.ovk?.rows?.length > 0
}
/>
)}
</Card>
<Card
title='Pakan'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
>
{isLoading ? (
<SapronakCalculationClosingSkeleton columns={pakanColumns} />
) : isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.pakan?.rows?.length === 0 ? (
<SapronakCalculationClosingSkeleton
columns={pakanColumns}
iconName='heroicons:chart-bar'
title='Data Perhitungan Sapronak Tidak Ditemukan'
subtitle='Tidak ada data perhitungan sapronak untuk periode ini.'
/>
) : (
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.pakan?.rows ?? [])
: []
}
columns={pakanColumns}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
renderFooter={
isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.pakan?.rows?.length > 0
}
/>
)}
</Card>
</div>
);
};
export default SapronakCalculationClosingTable;
@@ -1,20 +1,20 @@
'use client';
import { ChangeEventHandler, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { ColumnDef, SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Card from '@/components/Card';
import Collapse from '@/components/Collapse';
import Badge from '@/components/Badge';
import { cn, formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ClosingApi } from '@/services/api/closing';
import { ClosingIncomingSapronakSummary } from '@/types/api/closing';
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
interface ClosingIncomingSapronaksSummaryTableProps {
projectFlockId: number;
@@ -55,20 +55,60 @@ const ClosingIncomingSapronaksSummaryTable = ({
}
);
const [open, setOpen] = useState(true);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
[
{
header: '#',
header: 'No',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'category',
header: 'Kategori',
cell: (props) => {
const categories = props.row.original.category
.split(' ')
.filter((cat) => cat.trim());
const maxBadges = 4;
const visibleCategories = categories.slice(0, maxBadges);
const remainingCount = categories.length - maxBadges;
return (
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
{visibleCategories.map((category, index) => (
<Badge
key={index}
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
),
}}
title={category}
>
{category.length > 12
? `${category.slice(0, 12)}...`
: category}
</Badge>
))}
{remainingCount > 0 && (
<Badge
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
),
}}
title={categories.join(' ')}
>
+{remainingCount}
</Badge>
)}
</div>
);
},
},
{
accessorKey: 'total_qty',
@@ -78,10 +118,6 @@ const ClosingIncomingSapronaksSummaryTable = ({
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
@@ -93,44 +129,35 @@ const ClosingIncomingSapronaksSummaryTable = ({
}
}, [sorting, updateFilter]);
useEffect(() => {
if (!open) {
setOpen(
isResponseSuccess(incomingSapronakSummaries)
? incomingSapronakSummaries.data.length > 0
: false
);
}
}, [incomingSapronakSummaries, isResponseSuccess]);
return (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Ringkasan Sapronak Masuk</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
<div className='w-full'>
<Card
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Ringkasan Sapronak Masuk'
collapsible
defaultCollapsed={false}
>
<div className='w-full p-0'>
{isLoadingIncomingSapronakSummaries ? (
<SapronakClosingSkeleton
type='incoming'
columns={incomingSapronaksColumns}
/>
) : isResponseSuccess(incomingSapronakSummaries) &&
incomingSapronakSummaries.data.length === 0 ? (
<SapronakClosingSkeleton
type='incoming'
columns={incomingSapronaksColumns}
iconName='heroicons:chart-bar'
title='Ringkasan Sapronak Masuk Tidak Ditemukan'
subtitle='Tidak ada ringkasan sapronak masuk untuk periode ini.'
/>
) : (
<Table<ClosingIncomingSapronakSummary>
data={
isResponseSuccess(incomingSapronakSummaries)
@@ -158,16 +185,21 @@ const ClosingIncomingSapronaksSummaryTable = ({
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(incomingSapronakSummaries) &&
incomingSapronakSummaries?.data?.length === 0,
}),
containerClassName: 'w-full mb-5!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200',
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
/>
</div>
</Collapse>
</Card>
)}
</Card>
</div>
);
};
@@ -9,13 +9,14 @@ import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Card from '@/components/Card';
import Collapse from '@/components/Collapse';
import Badge from '@/components/Badge';
import { cn, formatDate, formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ClosingApi } from '@/services/api/closing';
import { ClosingIncomingSapronak } from '@/types/api/closing';
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
interface ClosingIncomingSapronaksTableProps {
projectFlockId: number;
@@ -51,14 +52,12 @@ const ClosingIncomingSapronaksTable = ({
ClosingApi.getAllIncomingSapronakFetcher
);
const [open, setOpen] = useState(true);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronak>[] = [
{
header: '#',
header: 'No',
cell: (props) => props.row.index + 1,
},
{
@@ -81,6 +80,48 @@ const ClosingIncomingSapronaksTable = ({
{
accessorKey: 'product_category',
header: 'Kategori Produk',
cell: (props) => {
const categories = props.row.original.product_category
.split(' ')
.filter((cat) => cat.trim());
const maxBadges = 4;
const visibleCategories = categories.slice(0, maxBadges);
const remainingCount = categories.length - maxBadges;
return (
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
{visibleCategories.map((category, index) => (
<Badge
key={index}
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
),
}}
title={category}
>
{category.length > 12
? `${category.slice(0, 12)}...`
: category}
</Badge>
))}
{remainingCount > 0 && (
<Badge
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
),
}}
title={categories.join(' ')}
>
+{remainingCount}
</Badge>
)}
</div>
);
},
},
{
accessorKey: 'source_warehouse',
@@ -117,56 +158,59 @@ const ClosingIncomingSapronaksTable = ({
}
}, [sorting, updateFilter]);
useEffect(() => {
if (!open) {
setOpen(
isResponseSuccess(incomingSapronaks)
? incomingSapronaks.data.length > 0
: false
);
}
}, [incomingSapronaks, isResponseSuccess]);
return (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Sapronak Masuk</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Sapronak Masuk'
collapsible
defaultCollapsed={false}
>
<div className='flex flex-col gap-2 my-4'>
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Sapronak Masuk'
value={tableFilterState.search}
onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
className={{
wrapper: 'w-full min-w-24 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
>
<div className='w-full p-0'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Sapronak Masuk'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
</div>
</div>
{isLoadingIncomingSapronaks ? (
<SapronakClosingSkeleton
type='incoming'
columns={incomingSapronaksColumns}
/>
) : isResponseSuccess(incomingSapronaks) &&
incomingSapronaks.data.length === 0 ? (
<SapronakClosingSkeleton
type='incoming'
columns={incomingSapronaksColumns}
iconName='heroicons:chart-bar'
title='Data Sapronak Masuk Tidak Ditemukan'
subtitle='Tidak ada data sapronak masuk untuk periode ini.'
/>
) : (
<Table<ClosingIncomingSapronak>
data={
isResponseSuccess(incomingSapronaks)
@@ -194,16 +238,21 @@ const ClosingIncomingSapronaksTable = ({
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(incomingSapronaks) &&
incomingSapronaks?.data?.length === 0,
}),
containerClassName: 'w-full mb-5!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200',
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
/>
</div>
</Collapse>
</Card>
)}
</Card>
</div>
);
};
@@ -1,20 +1,20 @@
'use client';
import { ChangeEventHandler, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { ColumnDef, SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Card from '@/components/Card';
import Collapse from '@/components/Collapse';
import Badge from '@/components/Badge';
import { cn, formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ClosingApi } from '@/services/api/closing';
import { ClosingOutgoingSapronakSummary } from '@/types/api/closing';
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
interface ClosingOutgoingSapronaksSummaryTableProps {
projectFlockId: number;
@@ -55,20 +55,60 @@ const ClosingOutgoingSapronaksSummaryTable = ({
}
);
const [open, setOpen] = useState(true);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
[
{
header: '#',
header: 'No',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'category',
header: 'Kategori',
cell: (props) => {
const categories = props.row.original.category
.split(' ')
.filter((cat) => cat.trim());
const maxBadges = 4;
const visibleCategories = categories.slice(0, maxBadges);
const remainingCount = categories.length - maxBadges;
return (
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
{visibleCategories.map((category, index) => (
<Badge
key={index}
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
),
}}
title={category}
>
{category.length > 12
? `${category.slice(0, 12)}...`
: category}
</Badge>
))}
{remainingCount > 0 && (
<Badge
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
),
}}
title={categories.join(' ')}
>
+{remainingCount}
</Badge>
)}
</div>
);
},
},
{
accessorKey: 'total_qty',
@@ -78,10 +118,6 @@ const ClosingOutgoingSapronaksSummaryTable = ({
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
@@ -93,44 +129,35 @@ const ClosingOutgoingSapronaksSummaryTable = ({
}
}, [sorting, updateFilter]);
useEffect(() => {
if (!open) {
setOpen(
isResponseSuccess(outgoingSapronakSummaries)
? outgoingSapronakSummaries.data.length > 0
: false
);
}
}, [outgoingSapronakSummaries, isResponseSuccess]);
return (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Ringkasan Sapronak Keluar</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
<div className='w-full'>
<Card
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Ringkasan Sapronak Keluar'
collapsible
defaultCollapsed={false}
>
<div className='w-full p-0'>
{isLoadingOutgoingSapronakSummaries ? (
<SapronakClosingSkeleton
type='outgoing'
columns={outgoingSapronaksColumns}
/>
) : isResponseSuccess(outgoingSapronakSummaries) &&
outgoingSapronakSummaries.data.length === 0 ? (
<SapronakClosingSkeleton
type='outgoing'
columns={outgoingSapronaksColumns}
iconName='heroicons:chart-bar'
title='Ringkasan Sapronak Keluar Tidak Ditemukan'
subtitle='Tidak ada ringkasan sapronak keluar untuk periode ini.'
/>
) : (
<Table<ClosingOutgoingSapronakSummary>
data={
isResponseSuccess(outgoingSapronakSummaries)
@@ -158,16 +185,21 @@ const ClosingOutgoingSapronaksSummaryTable = ({
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(outgoingSapronakSummaries) &&
outgoingSapronakSummaries?.data?.length === 0,
}),
containerClassName: 'w-full mb-5!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200',
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
/>
</div>
</Collapse>
</Card>
)}
</Card>
</div>
);
};
@@ -9,13 +9,16 @@ import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Card from '@/components/Card';
import Collapse from '@/components/Collapse';
import Badge from '@/components/Badge';
import { cn, formatDate, formatNumber } from '@/lib/helper';
import { cn } from '@/lib/helper';
import { formatDate, formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ClosingApi } from '@/services/api/closing';
import { ClosingOutgoingSapronak } from '@/types/api/closing';
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
interface ClosingOutgoingSapronaksTableProps {
projectFlockId: number;
@@ -51,14 +54,12 @@ const ClosingOutgoingSapronaksTable = ({
ClosingApi.getAllOutgoingSapronakFetcher
);
const [open, setOpen] = useState(true);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronak>[] = [
{
header: '#',
header: 'No',
cell: (props) => props.row.index + 1,
},
{
@@ -81,6 +82,48 @@ const ClosingOutgoingSapronaksTable = ({
{
accessorKey: 'product_category',
header: 'Kategori Produk',
cell: (props) => {
const categories = props.row.original.product_category
.split(' ')
.filter((cat) => cat.trim());
const maxBadges = 4;
const visibleCategories = categories.slice(0, maxBadges);
const remainingCount = categories.length - maxBadges;
return (
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
{visibleCategories.map((category, index) => (
<Badge
key={index}
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
),
}}
title={category}
>
{category.length > 12
? `${category.slice(0, 12)}...`
: category}
</Badge>
))}
{remainingCount > 0 && (
<Badge
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
),
}}
title={categories.join(' ')}
>
+{remainingCount}
</Badge>
)}
</div>
);
},
},
{
accessorKey: 'source_warehouse',
@@ -117,56 +160,59 @@ const ClosingOutgoingSapronaksTable = ({
}
}, [sorting, updateFilter]);
useEffect(() => {
if (!open) {
setOpen(
isResponseSuccess(outgoingSapronaks)
? outgoingSapronaks.data.length > 0
: false
);
}
}, [outgoingSapronaks, isResponseSuccess]);
return (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Sapronak Keluar</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
<div className='w-full'>
<Card
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Sapronak Keluar'
collapsible
defaultCollapsed={false}
>
<div className='flex flex-col gap-2 my-4'>
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Sapronak Keluar'
value={tableFilterState.search}
onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
className={{
wrapper: 'w-full min-w-24 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
>
<div className='w-full p-0'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Sapronak Keluar'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
</div>
</div>
{isLoadingOutgoingSapronaks ? (
<SapronakClosingSkeleton
type='outgoing'
columns={outgoingSapronaksColumns}
/>
) : isResponseSuccess(outgoingSapronaks) &&
outgoingSapronaks.data.length === 0 ? (
<SapronakClosingSkeleton
type='outgoing'
columns={outgoingSapronaksColumns}
iconName='heroicons:chart-bar'
title='Data Sapronak Keluar Tidak Ditemukan'
subtitle='Tidak ada data sapronak keluar untuk periode ini.'
/>
) : (
<Table<ClosingOutgoingSapronak>
data={
isResponseSuccess(outgoingSapronaks)
@@ -194,16 +240,21 @@ const ClosingOutgoingSapronaksTable = ({
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(outgoingSapronaks) &&
outgoingSapronaks?.data?.length === 0,
}),
containerClassName: 'w-full mb-5!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200',
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
/>
</div>
</Collapse>
</Card>
)}
</Card>
</div>
);
};
@@ -256,7 +256,7 @@ export const generateDashboardPDF = async ({
pdf.save(fileName);
toast.success('PDF exported successfully!', { id: 'export-pdf' });
} catch (error) {
} catch {
toast.error('Failed to export PDF. Please try again.', {
id: 'export-pdf',
});
@@ -49,7 +49,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => {
color={expenseStatusBadgeColor}
text={isLatestApprovalRejected ? 'Ditolak' : (approval?.step_name ?? '')}
className={{
badge: 'w-fit',
badge: 'whitespace-nowrap max-w-max w-fit',
}}
/>
);
@@ -276,6 +276,13 @@ const ExpensesTable = () => {
);
},
},
{
accessorKey: 'reference_number',
header: 'Nomor Referensi',
cell: (props) => {
return props.row.original.reference_number ?? '-';
},
},
{
accessorKey: 'transaction_date',
header: 'Tanggal Pengajuan',
@@ -29,7 +29,7 @@ const RealizationStatusBadge = ({ approval }: RealizationStatusBadgeProps) => {
color={realizationStatusBadgeColor}
text={isLatestApprovalRejected ? 'Ditolak' : realizationStatus}
className={{
badge: 'w-fit',
badge: 'whitespace-nowrap max-w-max w-fit',
}}
/>
);
+185 -114
View File
@@ -1,13 +1,7 @@
import {
ChangeEventHandler,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { CellContext } from '@tanstack/react-table';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { useFormik } from 'formik';
import Button from '@/components/Button';
import Card from '@/components/Card';
@@ -40,6 +34,10 @@ import { Icon } from '@iconify/react';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import { useUiStore } from '@/stores/ui/ui.store';
import {
FinanceTableFilterSchema,
FinanceTableFilterValues,
} from './FinanceTableFilter.schema';
const RowOptionsMenu = ({
type = 'dropdown',
@@ -152,10 +150,10 @@ const FinanceTable = () => {
} = useTableFilter({
initial: {
search: searchValue,
transactionType: '',
bankId: '',
customerId: '',
supplierId: '',
transactionTypes: '',
bankIds: '',
customerIds: '',
supplierIds: '',
sortBy: '',
startDate: '',
endDate: '',
@@ -163,10 +161,10 @@ const FinanceTable = () => {
paramMap: {
page: 'page',
pageSize: 'limit',
transactionType: 'transaction_type',
bankId: 'bank_id',
customerId: 'customer_id',
supplierId: 'supplier_id',
transactionTypes: 'transaction_types',
bankIds: 'bank_ids',
customerIds: 'customer_ids',
supplierIds: 'supplier_ids',
sortBy: 'sort_date',
startDate: 'start_date',
endDate: 'end_date',
@@ -174,18 +172,7 @@ const FinanceTable = () => {
});
// ===== State =====
const [searchParams, setSearchParams] = useSearchParams();
const deleteModal = useModal();
const [pendingFilters, setPendingFilters] = useState({
search: searchValue,
transactionType: '',
bankId: '',
customerId: '',
supplierId: '',
sortBy: '',
startDate: '',
endDate: '',
});
const [selectedTransactionType, setSelectedTransactionType] = useState<
OptionType | OptionType[] | null
>(null);
@@ -201,6 +188,34 @@ const FinanceTable = () => {
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [dateErrorShown, setDateErrorShown] = useState(false);
// ===== Formik for Filter =====
const filterFormik = useFormik<FinanceTableFilterValues>({
initialValues: {
search: searchValue,
transaction_types: '',
bank_ids: '',
customer_ids: '',
supplier_ids: '',
sort_by: '',
start_date: '',
end_date: '',
},
validationSchema: FinanceTableFilterSchema,
enableReinitialize: true,
onSubmit: (values) => {
updateFilter('search', values.search);
setSearchValue(values.search);
updateFilter('transactionTypes', values.transaction_types);
updateFilter('bankIds', values.bank_ids);
updateFilter('customerIds', values.customer_ids);
updateFilter('supplierIds', values.supplier_ids);
updateFilter('sortBy', values.sort_by);
updateFilter('startDate', values.start_date);
updateFilter('endDate', values.end_date);
},
});
// ===== Fetch Data =====
const {
@@ -237,84 +252,141 @@ const FinanceTable = () => {
loadMore: bankLoadMore,
} = useSelect<Bank>(BankApi.basePath, 'id', 'alias');
const bankSelectOptions = useMemo(() => {
if (!isResponseSuccess(bankRawData)) return [];
return bankOptions.map((bank) => {
const bankData = bankRawData.data.find((data) => data.id === bank?.value);
return {
label: bankData
? `${bankData.alias} - ${bankData.account_number} - ${bankData.owner}`
: '',
value: bank?.value,
};
});
}, [bankOptions, bankRawData]);
// ===== Handler =====
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setPendingFilters((prev) => ({ ...prev, search: e.target.value }));
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
filterFormik.setFieldValue('search', e.target.value);
};
const transactionTypeChangeHandler = (
val: OptionType | OptionType[] | null
) => {
setSelectedTransactionType(val);
setPendingFilters((prev) => ({
...prev,
transactionType: val
filterFormik.setFieldValue(
'transaction_types',
val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
}));
: ''
);
};
const bankChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedBank(val);
setPendingFilters((prev) => ({
...prev,
bankId: val
filterFormik.setFieldValue(
'bank_ids',
val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
}));
: ''
);
};
const customerIdChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedCustomerId(val);
setPendingFilters((prev) => ({
...prev,
customerId: val
filterFormik.setFieldValue(
'customer_ids',
val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
}));
: ''
);
};
const supplierIdChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedSupplierId(val);
setPendingFilters((prev) => ({
...prev,
supplierId: val
filterFormik.setFieldValue(
'supplier_ids',
val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
}));
: ''
);
};
const sortByChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedSortBy(val as OptionType);
setPendingFilters((prev) => ({
...prev,
sortBy: val ? ((val as OptionType).value as string) : '',
}));
filterFormik.setFieldValue(
'sort_by',
val ? ((val as OptionType).value as string) : ''
);
};
const startDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setPendingFilters((prev) => ({ ...prev, startDate: e.target.value }));
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const endDate = filterFormik.values.end_date;
filterFormik.setFieldValue('start_date', value);
if (value && endDate) {
const startDate = new Date(value);
const endDateObj = new Date(endDate);
if (endDateObj < startDate) {
filterFormik.setFieldError(
'end_date',
'Tanggal akhir tidak boleh masa lampau'
);
if (!dateErrorShown) {
toast.error('Tanggal akhir tidak boleh masa lampau', {
duration: Infinity,
});
setDateErrorShown(true);
}
} else {
filterFormik.setFieldError('end_date', undefined);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
}
}
};
const endDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setPendingFilters((prev) => ({ ...prev, endDate: e.target.value }));
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
};
const submitFilterHandler = () => {
updateFilter('search', pendingFilters.search);
setSearchValue(pendingFilters.search);
updateFilter('transactionType', pendingFilters.transactionType);
updateFilter('bankId', pendingFilters.bankId);
updateFilter('customerId', pendingFilters.customerId);
updateFilter('supplierId', pendingFilters.supplierId);
updateFilter('sortBy', pendingFilters.sortBy);
updateFilter('startDate', pendingFilters.startDate);
updateFilter('endDate', pendingFilters.endDate);
const endDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const startDate = filterFormik.values.start_date;
filterFormik.setFieldValue('end_date', value);
if (value && startDate) {
const startDateObj = new Date(startDate);
const endDate = new Date(value);
if (endDate < startDateObj) {
filterFormik.setFieldError(
'end_date',
'Tanggal akhir tidak boleh masa lampau'
);
if (!dateErrorShown) {
toast.error('Tanggal akhir tidak boleh masa lampau', {
duration: Infinity,
});
setDateErrorShown(true);
}
return;
}
}
filterFormik.setFieldError('end_date', undefined);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
};
const resetFilterHandler = () => {
setSelectedTransactionType(null);
setSelectedBank(null);
@@ -322,24 +394,14 @@ const FinanceTable = () => {
setSelectedSupplierId(null);
setSelectedSortBy(null);
const emptyFilters = {
search: '',
transactionType: '',
bankId: '',
customerId: '',
supplierId: '',
sortBy: '',
startDate: '',
endDate: '',
};
setPendingFilters(emptyFilters);
filterFormik.resetForm();
updateFilter('search', '');
resetSearchValue();
updateFilter('transactionType', '');
updateFilter('bankId', '');
updateFilter('customerId', '');
updateFilter('supplierId', '');
updateFilter('transactionTypes', '');
updateFilter('bankIds', '');
updateFilter('customerIds', '');
updateFilter('supplierIds', '');
updateFilter('sortBy', '');
updateFilter('startDate', '');
updateFilter('endDate', '');
@@ -464,26 +526,36 @@ const FinanceTable = () => {
}, []);
useEffect(() => {
// Store current path on mount
return () => {
if (dateErrorShown) {
toast.dismiss();
}
};
}, [dateErrorShown]);
useEffect(() => {
previousPathRef.current = window.location.pathname;
return () => {
const currentPath = window.location.pathname;
// if both paths are within /finance module
const isCurrentPathFinance = currentPath.includes('/finance');
const isPreviousPathFinance =
previousPathRef.current?.includes('/finance');
// reset if we outside finance module entirely
if (isPreviousPathFinance && !isCurrentPathFinance) {
resetSearchValue();
}
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
};
}, [resetSearchValue]);
}, [resetSearchValue, dateErrorShown]);
return (
<section className='size-full p-6 flex flex-col gap-6'>
<section className='size-full flex flex-col gap-6'>
<div className='flex justify-end gap-2'>
<RequirePermission permissions='lti.finance.injections.create'>
<Button
@@ -526,7 +598,7 @@ const FinanceTable = () => {
<Button
color='primary'
className='min-w-24'
onClick={submitFilterHandler}
onClick={() => filterFormik.handleSubmit()}
>
Cari
</Button>
@@ -539,6 +611,7 @@ const FinanceTable = () => {
label='Jenis Transaksi'
value={selectedTransactionType}
onChange={transactionTypeChangeHandler}
closeMenuOnSelect={false}
isClearable
isMulti
/>
@@ -550,6 +623,7 @@ const FinanceTable = () => {
onInputChange={customerInputValue}
onMenuScrollToBottom={customerLoadMore}
isLoading={customerIsLoadingOptions}
closeMenuOnSelect={false}
isClearable
isMulti
/>
@@ -561,31 +635,18 @@ const FinanceTable = () => {
onInputChange={supplierInputValue}
onMenuScrollToBottom={supplierLoadMore}
isLoading={supplierIsLoadingOptions}
closeMenuOnSelect={false}
isClearable
isMulti
/>
<SelectInput
options={
isResponseSuccess(bankRawData)
? bankOptions.map((bank) => ({
label:
bankRawData.data.find((data) => data.id === bank?.value)
?.alias +
' - ' +
bankRawData.data.find((data) => data.id === bank?.value)
?.account_number +
' - ' +
bankRawData.data.find((data) => data.id === bank?.value)
?.owner,
value: bank?.value,
}))
: []
}
options={bankSelectOptions}
label='Bank'
value={selectedBank}
onChange={bankChangeHandler}
onInputChange={bankInputValue}
onMenuScrollToBottom={bankLoadMore}
closeMenuOnSelect={false}
isClearable
isMulti
/>
@@ -597,22 +658,32 @@ const FinanceTable = () => {
isClearable
/>
<DateInput
name='startDate'
name='start_date'
label='Periode Tanggal (Mulai)'
value={pendingFilters.startDate}
value={filterFormik.values.start_date}
onChange={startDateChangeHandler}
errorMessage={
filterFormik.errors.end_date
? filterFormik.errors.end_date
: undefined
}
/>
<DateInput
name='endDate'
name='end_date'
label='Periode Tanggal (Akhir)'
value={pendingFilters.endDate}
value={filterFormik.values.end_date}
onChange={endDateChangeHandler}
errorMessage={
filterFormik.errors.end_date
? filterFormik.errors.end_date
: undefined
}
/>
<DebouncedTextInput
name='search'
label='Cari'
placeholder='Cari'
value={pendingFilters.search}
value={filterFormik.values.search}
onChange={searchChangeHandler}
/>
</div>
@@ -0,0 +1,38 @@
import * as yup from 'yup';
export type FinanceTableFilterType = {
search: string;
transaction_types: string;
bank_ids: string;
customer_ids: string;
supplier_ids: string;
sort_by: string;
start_date: string;
end_date: string;
};
export const FinanceTableFilterSchema = yup.object({
search: yup.string().optional(),
transaction_types: yup.string().optional(),
bank_ids: yup.string().optional(),
customer_ids: yup.string().optional(),
supplier_ids: yup.string().optional(),
sort_by: yup.string().optional(),
start_date: yup.string().optional(),
end_date: yup
.string()
.optional()
.test(
'is-greater-than-start',
'Tanggal akhir tidak boleh masa lampau',
function (value) {
const { start_date } = this.parent;
if (!start_date || !value) return true;
return new Date(value) >= new Date(start_date);
}
),
}) as yup.ObjectSchema<FinanceTableFilterType>;
export type FinanceTableFilterValues = yup.InferType<
typeof FinanceTableFilterSchema
>;
@@ -48,9 +48,9 @@ const FormFinanceAddInitialBalance = ({
// ===== Formik =====
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
// Type assertion to handle potential initial_balance_type field
const extendedInitialValues = initialValues as Finance & {
initial_balance_type?: string;
};
// const extendedInitialValues = initialValues as Finance & {
// initial_balance_type?: string;
// };
return {
party_type_option:
@@ -122,8 +122,6 @@ const FormFinanceAddInitialBalance = ({
options: bankOptions,
rawData: bankRawData,
isLoadingOptions: isLoadingBankOptions,
setInputValue: setBankInputValue,
loadMore: loadMoreBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
// ===== Helper Functions =====
@@ -28,10 +28,7 @@ import { useCallback, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import Alert from '@/components/Alert';
import { Icon } from '@iconify/react';
import {
FINANCE_INJECTION_STATUS,
FINANCE_INJECTION_TYPE_OPTIONS,
} from '@/config/constant';
import { FINANCE_INJECTION_TYPE_OPTIONS } from '@/config/constant';
interface FormFinanceInjectionProps {
type?: 'add' | 'edit';
@@ -15,7 +15,6 @@ import { Icon } from '@iconify/react';
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
import { useCallback, useEffect, useState } from 'react';
import useSWR from 'swr';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
const InventoryAdjustmentTable = () => {
const {
@@ -14,7 +14,6 @@ import {
InventoryAdjustmentFormSchema,
InventoryAdjustmentFormValues,
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
import useSWR from 'swr';
import {
ProductApi,
ProductCategoryApi,
@@ -33,7 +33,6 @@ import { toast } from 'react-hot-toast';
import { MovementApi } from '@/services/api/inventory';
import FileInput from '@/components/input/FileInput';
import CheckboxInput from '@/components/input/CheckboxInput';
import Badge from '@/components/Badge';
import Card from '@/components/Card';
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
import { getUniqueFormikErrors } from '@/lib/formik-helper';

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