From 4b6144d0b49d7e3c2246e0c6f8828b00427cdd93 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 27 Nov 2025 13:36:12 +0700 Subject: [PATCH 01/38] refactor(FE-Storyless): update import paths for schema files to use absolute paths --- src/components/Card.tsx | 2 +- .../pages/production/recording/form/RecordingForm.tsx | 2 +- .../pages/production/recording/grading/form/GradingForm.tsx | 2 +- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 2 +- .../purchase/form/order/PurchaseOrderStaffApprovalForm.tsx | 2 +- .../pages/purchase/form/request/PurchaseRequestForm.tsx | 3 +-- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index d3ff80b1..ff4c35f2 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -4,7 +4,7 @@ import { HTMLAttributes, ReactNode, useState } from 'react'; import { cn } from '@/lib/helper'; import Image from 'next/image'; -import Collapse from './Collapse'; +import Collapse from '@/components/Collapse'; import { Icon } from '@iconify/react'; export interface CardProps diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 5900c84a..52297258 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -48,7 +48,7 @@ import { getRecordingLayingFormInitialValues, UpdateRecordingGrowingFormSchema, UpdateRecordingLayingFormSchema, -} from './RecordingForm.schema'; +} from '@/components/pages/production/recording/form/RecordingForm.schema'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 9c3ba37a..1e91c78d 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -28,7 +28,7 @@ import { RecordingGradingFormValues, UpdateRecordingGradingFormSchema, getRecordingGradingFormInitialValues, -} from '../../form/RecordingForm.schema'; +} from '@/components/pages/production/recording/form/RecordingForm.schema'; import { cn, formatDate } from '@/lib/helper'; import toast from 'react-hot-toast'; diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 7909ade9..79762da9 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -18,7 +18,7 @@ import { PurchaseRequestAcceptApprovalFormDefaultValues, PurchaseRequestAcceptApprovalFormInitialValues, PurchaseRequestAcceptApprovalFormSchema, -} from './PurchaseOrderForm.schema'; +} from '@/components/pages/purchase/form/order/PurchaseOrderForm.schema'; import { isResponseError } from '@/lib/api-helper'; import { PurchaseApi } from '@/services/api/purchase'; import { diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 63756ad9..69c3fd13 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -21,7 +21,7 @@ import { PurchaseRequestStaffApprovalFormInitialValues, PurchaseRequestStaffApprovalFormSchema, PurchaseStaffApprovalItemSchema, -} from './PurchaseOrderForm.schema'; +} from '@/components/pages/purchase/form/order/PurchaseOrderForm.schema'; import { isResponseError } from '@/lib/api-helper'; import { formatNumber } from '@/lib/helper'; import { PurchaseApi } from '@/services/api/purchase'; diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 7100b134..396ce7bb 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -22,13 +22,12 @@ import { PurchaseRequestFormValues, getPurchaseRequestFormInitialValues, UpdatePurchaseRequestFormSchema, -} from './PurchaseRequestForm.schema'; +} from '@/components/pages/purchase/form/request/PurchaseRequestForm.schema'; import { SupplierApi, AreaApi, LocationApi, WarehouseApi, - ProductApi, } from '@/services/api/master-data'; import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; From 7a76719547358f67d25a2d285772f5ad099d450d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 27 Nov 2025 13:46:53 +0700 Subject: [PATCH 02/38] refactor(FE-Storyless): remove console, window and err catch --- .../pages/production/recording/form/RecordingForm.tsx | 3 +-- .../production/recording/grading/form/GradingForm.tsx | 3 +-- .../form/order/PurchaseOrderStaffApprovalForm.tsx | 3 +-- .../pages/purchase/order/PurchaseOrderInvoice.tsx | 8 ++++---- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 52297258..43ffc98b 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2924,8 +2924,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, 1000); } } - } catch (error) { - console.error('Error creating recording:', error); + } catch { toast.error( 'Gagal membuat recording. Silakan coba lagi.' ); diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 1e91c78d..417c6356 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -173,8 +173,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { deleteModal.closeModal(); toast.success(res?.message || 'Successfully delete Grading!'); router.push('/production/recording'); - } catch (err) { - console.error(err); + } catch { setGradingFormErrorMessage('Failed to delete Grading'); } finally { setIsDeleteLoading(false); diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 69c3fd13..791e2592 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -241,9 +241,8 @@ const PurchaseOrderStaffApprovalForm = ({ ); formik.setFieldValue('items', updatedPurchaseItems); } - } catch (error) { + } catch { toast.error('Terjadi kesalahan saat menghapus item pembelian'); - console.error('Delete item error:', error); } }, [ initialValues?.id, diff --git a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx index d7497d7e..36aea9c7 100644 --- a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx @@ -12,6 +12,7 @@ import { pdf, } from '@react-pdf/renderer'; import { Icon } from '@iconify/react'; +import toast from 'react-hot-toast'; import Button from '@/components/Button'; import { Purchase } from '@/types/api/purchase/purchase'; @@ -251,7 +252,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { const handleDownloadPDF = async () => { if (!purchaseData) { - alert('No purchase order data available'); + toast.error('No purchase order data available'); return; } @@ -502,9 +503,8 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); - } catch (error) { - console.error('Error generating PDF:', error); - alert('Failed to generate PDF. Please try again.'); + } catch { + toast.error('Failed to generate PDF. Please try again.'); } finally { setIsGeneratingPDF(false); } From 711deda6a8391f0d84c757d4bd3da478ea5aee17 Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 28 Nov 2025 09:38:44 +0000 Subject: [PATCH 03/38] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 951e5472..6c280dbd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -106,8 +106,11 @@ build:dev: environment: name: development variables: - NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id' - NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id' + # NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id' + # NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id' + NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id/' + NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id' + NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api' deploy:dev: <<: *deploy_template From 5046d687b518fba4533262fadbc337b87b3920ee Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 28 Nov 2025 09:54:57 +0000 Subject: [PATCH 04/38] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6c280dbd..b035f668 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -106,11 +106,11 @@ build:dev: environment: name: development variables: - # NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id' - # NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id' - NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id/' - NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id' - NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api' + NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id' + NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id' + # NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id/' + # NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id' + # NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api' deploy:dev: <<: *deploy_template From 944f479e2d910241550b441b9eac5d1d4e5e4041 Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 28 Nov 2025 10:02:17 +0000 Subject: [PATCH 05/38] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b035f668..6c280dbd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -106,11 +106,11 @@ build:dev: environment: name: development variables: - NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id' - NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id' - # NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id/' - # NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id' - # NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api' + # NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id' + # NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id' + NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id/' + NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id' + NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api' deploy:dev: <<: *deploy_template From 94e2d71dba702a10b0a20847c5ea199d43dac155 Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 28 Nov 2025 10:15:49 +0000 Subject: [PATCH 06/38] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6c280dbd..26c92940 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -108,7 +108,7 @@ build:dev: variables: # NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id' # NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-api-sso.mbugroup.id' - NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id/' + NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id' NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id' NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api' From 4073f4dfde5c0d78a36078e5c3ca41c7ff542e00 Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 28 Nov 2025 12:47:47 +0000 Subject: [PATCH 07/38] Update .gitlab-ci.yml update env --- .gitlab-ci.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 26c92940..67c077da 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,8 +15,24 @@ stages: script: - echo "Installing dependencies..." - npm ci --no-audit --no-fund + - echo "Build env used:" + - echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL" + - echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL" + - echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL" - echo "Building Next.js static export..." - npx next build + - | + mkdir -p out + cat < out/build-info.json + { + "commit": "$CI_COMMIT_SHORT_SHA", + "pipeline": "$CI_PIPELINE_ID", + "built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", + "NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL", + "NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL", + "NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL" + } + EOF artifacts: name: 'out-$CI_COMMIT_SHORT_SHA' paths: @@ -145,5 +161,5 @@ deploy:dev: # CLOUDFRONT_DISTRIBUTION_ID: "ddfd" # environment: # name: production -# url: https://royalgoldcapital.com + From 58532881f4c8932c63300e8791b9b8361534233e Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 28 Nov 2025 13:06:03 +0000 Subject: [PATCH 08/38] Update .gitlab-ci.yml update env --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 67c077da..91da62b9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,6 +140,7 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id + # ====== PRODUCTION ====== # build:production: # <<: *build_template From 8fbe6aa148d0268fb7f53337e2e98a1694e533dd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 3 Dec 2025 22:26:33 +0700 Subject: [PATCH 09/38] chore(FE-Storyless): Add .claude to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d86875dd..e47b8ec3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ next-env.d.ts # idea .idea + +# claude +.claude From 50559caf52a0af86bbb9fd3545b4282b5f7e71a5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 3 Dec 2025 22:28:18 +0700 Subject: [PATCH 10/38] feat(FE-326): Support custom header rows and cell render hook --- src/components/Table.tsx | 155 +++++++++++++++++++++++++++------------ 1 file changed, 110 insertions(+), 45 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b02dd3b5..eafd3e7a 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,6 +14,8 @@ import { SortingState, OnChangeFn, Row, + HeaderGroup, + Column, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -32,6 +34,20 @@ interface TableClassNames { bodyRowClassName?: string; bodyColumnClassName?: string; paginationClassName?: string; + customHeaderRowClassName?: string; + customHeaderCellClassName?: string; +} + +export interface CustomHeaderRow { + id: string; + cells: Array<{ + id: string; + content: ReactNode; + colSpan?: number; + rowSpan?: number; + className?: string; + }>; + className?: string; } export interface TableProps { @@ -52,6 +68,13 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); + customHeaderRows?: CustomHeaderRow[]; + renderCustomHeaders?: boolean; + onCustomHeaderCellRender?: ( + cell: ReactNode, + column: Column, + headerGroup: HeaderGroup + ) => ReactNode; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -85,6 +108,8 @@ const Table = ({ bodyRowClassName: '', bodyColumnClassName: '', paginationClassName: '', + customHeaderRowClassName: '', + customHeaderCellClassName: '', }, emptyContent = emptyContentDefaultValue, sorting, @@ -93,6 +118,9 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, + customHeaderRows = [], + renderCustomHeaders = false, + onCustomHeaderCellRender, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -195,55 +223,92 @@ const Table = ({
+ {renderCustomHeaders && + customHeaderRows.length > 0 && + customHeaderRows.map((headerRow) => ( + + {headerRow.cells.map((cell) => ( + + ))} + + ))} + {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => ( - - ))} + > +
+ {cellContent} + + {header.column.getCanSort() && ( +
+ + +
+ )} +
+ + ); + })} ))} From 3a87b039bfecfa51b3f8a892900fe2df236752bf Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 3 Dec 2025 22:31:10 +0700 Subject: [PATCH 11/38] feat(FE-326): Add SalesReportTable component --- .../pages/closing/sale/SalesReportTable.tsx | 554 ++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 src/components/pages/closing/sale/SalesReportTable.tsx diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx new file mode 100644 index 00000000..525d1dcf --- /dev/null +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -0,0 +1,554 @@ +'use client'; + +import Tabs from '@/components/Tabs'; +import React, { useState, useMemo } from 'react'; +import { ColumnDef } from '@tanstack/react-table'; +import Table, { CustomHeaderRow } from '@/components/Table'; +import Card from '@/components/Card'; +import Badge from '@/components/Badge'; +import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; + +type BaseClosingSales = { + id: number; + realization_date: string; + week_age: number; + age_label: string; + delivery_order_number: string; + product: string; + product_type: string; + customer: string; + quantity: number; + weight: number; + average: number; + price: number; + total: number; + kandang: string; + kandang_id: number; + payment_status: string; +}; + +const generateCustomHeaders = (template: { + groups: Array<{ + label: string; + field?: string; + rowSpan?: number; + colSpan?: number; + subLabels?: string[]; + }>; +}): CustomHeaderRow[] => { + const mainRow: Array<{ + id: string; + content: React.ReactNode; + colSpan?: number; + rowSpan?: number; + className: string; + }> = []; + const subRow: Array<{ + id: string; + content: React.ReactNode; + colSpan?: number; + rowSpan?: number; + className: string; + }> = []; + let subColumnIndex = 0; + + template.groups.forEach((group) => { + if (group.subLabels) { + mainRow.push({ + id: `${group.field || 'group'}-${subColumnIndex}`, + content: group.label, + colSpan: group.colSpan, + className: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-300', + }); + + group.subLabels.forEach((subLabel) => { + subRow.push({ + id: `sub-${subColumnIndex}`, + content: subLabel, + className: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300 border-t-0', + }); + subColumnIndex++; + }); + } else { + mainRow.push({ + id: `${group.field}-header`, + content: group.label, + rowSpan: group.rowSpan, + className: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300', + }); + } + }); + + const rows: CustomHeaderRow[] = [ + { + id: 'main-header', + cells: mainRow, + className: 'bg-gray-50', + }, + ]; + + if (subRow.length > 0) { + rows.push({ + id: 'sub-header', + cells: subRow, + className: 'bg-gray-50', + }); + } + + return rows; +}; + +const SalesReportTable = () => { + const [activeTabId, setActiveTabId] = useState('penjualan'); + + const salesBroilerData: BaseClosingSales[] = useMemo( + () => [ + { + id: 1, + realization_date: '2025-10-02', + week_age: 3, + age_label: '3 Weeks', + delivery_order_number: 'DO.MBU.699', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'TIAN YUSTIAN', + quantity: 1045, + weight: 1000, + average: 0.96, + price: 25300, + total: 25300000, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 2, + realization_date: '2025-10-07', + week_age: 4, + age_label: '4 Weeks', + delivery_order_number: 'DO.MBU.1037', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'ZAENAL MUTAQIN', + quantity: 850, + weight: 1211.4, + average: 1.43, + price: 23700, + total: 28710180, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 3, + realization_date: '2025-10-09', + week_age: 4, + age_label: '4 Weeks', + delivery_order_number: 'DO.MBU.1107', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'CORNELIUS TONY KUSTANTO', + quantity: 560, + weight: 990, + average: 1.77, + price: 23100, + total: 22869000, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 4, + realization_date: '2025-10-09', + week_age: 0, + age_label: '', + delivery_order_number: 'DO.MBU.1108', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'CV. KOPO AB', + quantity: 1088, + weight: 1934.3, + average: 1.78, + price: 23100, + total: 44682330, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 5, + realization_date: '2025-10-09', + week_age: 0, + age_label: '', + delivery_order_number: 'DO.MBU.1110', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'H. MAMAN ROMANSAH', + quantity: 624, + weight: 1121.4, + average: 1.8, + price: 22960, + total: 25747344, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 6, + realization_date: '2025-10-09', + week_age: 0, + age_label: '', + delivery_order_number: 'DO.MBU.1133', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'PT. SAMUDERA MULIA LESTARI', + quantity: 624, + weight: 1102.3, + average: 1.77, + price: 23100, + total: 25463130, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + ], + [] + ); + + const totals = useMemo(() => { + const totalQuantity = salesBroilerData.reduce( + (sum, item) => sum + (item.quantity || 0), + 0 + ); + const totalWeight = salesBroilerData.reduce( + (sum, item) => sum + (item.weight || 0), + 0 + ); + const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; + + const validPriceItems = salesBroilerData.filter( + (item) => item.price != null + ); + const avgPricePartner = + validPriceItems.length > 0 + ? validPriceItems.reduce((sum, item) => sum + item.price, 0) / + validPriceItems.length + : 0; + + const totalPartner = salesBroilerData.reduce( + (sum, item) => sum + (item.total || 0), + 0 + ); + + const avgPriceAct = avgPricePartner; + const totalAct = totalPartner; + + return { + totalQuantity, + totalWeight, + avgWeight, + avgPricePartner, + totalPartner, + avgPriceAct, + totalAct, + }; + }, [salesBroilerData]); + + const salesColumns: ColumnDef[] = useMemo( + () => [ + { + id: 'realization_date', + accessorKey: 'realization_date', + header: 'Tanggal Realisasi', + cell: (props) => { + const date = props.row.original.realization_date; + return date ? formatDate(date, 'DD MMM YYYY') : '-'; + }, + }, + { + id: 'age_label', + accessorKey: 'age_label', + header: 'Umur', + cell: (props) => props.getValue() || '-', + }, + { + id: 'delivery_order_number', + accessorKey: 'delivery_order_number', + header: 'No. DO', + cell: (props) => props.getValue() || '-', + }, + { + id: 'product', + accessorKey: 'product', + header: 'Produk', + cell: (props) => props.getValue() || '-', + }, + { + id: 'customer', + accessorKey: 'customer', + header: 'Customer', + cell: (props) => props.getValue() || '-', + }, + { + id: 'quantity', + accessorKey: 'quantity', + header: 'Ekor', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatNumber(value)} +
+ ); + }, + }, + { + id: 'weight', + accessorKey: 'weight', + header: 'Kg', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatNumber(value)} +
+ ); + }, + }, + { + id: 'average', + accessorKey: 'average', + header: 'AVG (Kg)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatNumber(value)} +
+ ); + }, + }, + { + id: 'price_partner', + accessorKey: 'price', + header: 'Harga Mitra (Rp)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'total_mitra', + accessorKey: 'total', + header: 'Total Mitra (Rp)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'price_act', + accessorKey: 'price', + header: 'Harga Act (Rp)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'total_act', + accessorKey: 'total', + header: 'Total Act (Rp)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'kandang', + accessorKey: 'kandang', + header: 'Kandang', + cell: (props) => props.getValue() || '-', + }, + { + id: 'payment_status', + accessorKey: 'payment_status', + header: 'Status Pembayaran', + cell: (props) => { + const status = props.getValue() as string; + const getStatusColor = (status: string) => { + if (!status) return 'neutral'; + switch (status.toLowerCase()) { + case 'lunas': + return 'success'; + case 'pending': + return 'warning'; + case 'belum lunas': + return 'error'; + default: + return 'neutral'; + } + }; + + return ( + + {status || '-'} + + ); + }, + }, + ], + [] + ); + + const headerTemplate = { + groups: [ + { label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 }, + { label: 'Umur', field: 'age_label', rowSpan: 2 }, + { label: 'No. DO', field: 'delivery_order_number', rowSpan: 2 }, + { label: 'Produk', field: 'product', rowSpan: 2 }, + { label: 'Customer', field: 'customer', rowSpan: 2 }, + { + label: 'Jumlah', + colSpan: 2, + subLabels: ['Ekor', 'Kg'], + }, + { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, + { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, + { label: 'Total Mitra (Rp)', field: 'total_partner', rowSpan: 2 }, + { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, + { label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 }, + { label: 'Kandang', field: 'kandang', rowSpan: 2 }, + { label: 'Status Pembayaran', field: 'payment_status', rowSpan: 2 }, + ], + }; + + const salesCustomHeaderRows = useMemo( + () => generateCustomHeaders(headerTemplate), + [] + ); + + return ( + <> +
+ +

+ Penjualan Ayam Besar +

+ +
+ {cell.content} +
-
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} + {headerGroup.headers.map((header) => { + let cellContent = flexRender( + header.column.columnDef.header, + header.getContext() + ); - {header.column.getCanSort() && ( -
- - -
+ if (onCustomHeaderCellRender) { + cellContent = onCustomHeaderCellRender( + cellContent, + header.column, + headerGroup + ); + } + + return ( +
-
+ + {/* Summary Row */} +
+ + + + + + + + + + + + + + + + + + +
+ Total Penjualan + + - + + - + + - + + - + + {formatNumber(totals.totalQuantity)} + + {formatNumber(totals.totalWeight)} + + {formatNumber(totals.avgWeight)} + + {formatCurrency(totals.avgPricePartner)} + + {formatCurrency(totals.totalPartner)} + + {formatCurrency(totals.avgPriceAct)} + + {formatCurrency(totals.totalAct)} + + - + + - +
+ +
+ ), + }, + ]} + variant='lifted' + /> + + + ); +}; + +export default SalesReportTable; From 411c2586f5c1e2bae689e84edeaab9a0150a6eb5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 3 Dec 2025 22:32:11 +0700 Subject: [PATCH 12/38] chore(format): prettier format --- .gitlab-ci.yml | 2 -- src/components/pages/production/recording/RecordingTable.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91da62b9..c37bfd35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -163,4 +162,3 @@ deploy:dev: # environment: # name: production - diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 6cf254e7..4a413bc4 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -370,7 +370,7 @@ const RecordingTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); - const [approvalNotes, setApprovalNotes] = useState(''); + const [, setApprovalNotes] = useState(''); const singleDeleteModal = useModal(); const approveModal = useModal(); From 991a594ee18a8d171aaa38c78b2de4acbf1fd97b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 11:51:11 +0700 Subject: [PATCH 13/38] refactor(FE-326): Add ClosingApiService and types for closing sales data --- src/services/api/closing.ts | 163 +++++++++++++++++++++++++++++ src/types/api/closing/closing.d.ts | 26 +++++ 2 files changed, 189 insertions(+) create mode 100644 src/services/api/closing.ts create mode 100644 src/types/api/closing/closing.d.ts diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts new file mode 100644 index 00000000..6dab6f8d --- /dev/null +++ b/src/services/api/closing.ts @@ -0,0 +1,163 @@ +import { BaseApiService } from './base'; +import { BaseApiResponse, CreatedUser } from '@/types/api/api-general'; +import { ClosingSales } from '@/types/api/closing/closing'; +import { sleep } from '@/lib/helper'; + +const adminUser: CreatedUser = { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', +}; + +const DUMMY_SALES: ClosingSales[] = [ + { + id: 1, + realization_date: '2025-01-15', + week_age: 4, + age_label: 'Week 4', + delivery_order_number: 'DO-2025-001', + product: { + id: 1, + name: 'Ayam Broiler', + brand: 'Brand A', + sku: 'AYM-001', + product_price: 20000, + selling_price: 25000, + tax: 10, + expiry_period: 30, + uom: { + id: 1, + name: 'Kg', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + product_category: { + id: 1, + code: 'LC001', + name: 'Live Chicken', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + suppliers: [], + flags: [], + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + product_category: { + id: 1, + code: 'LC001', + name: 'Live Chicken', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + customer: { + id: 1, + name: 'PT. Bumi Mandiri', + pic_id: 1, + pic: adminUser, + type: 'COMPANY', + address: 'Jl. Industri No. 123', + phone: '+62-21-1234567', + email: 'info@bumimandiri.com', + account_number: '1234567890', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + quantity: 1000, + weight: 1850, + average: 1.85, + price: 25000, + total: 25000000, + kandang: { + id: 1, + name: 'Singaparna 1', + status: 'ACTIVE', + location: { + id: 1, + name: 'Singaparna', + address: 'Jl. Singaparna No. 1', + area: { + id: 1, + name: 'Tasikmalaya', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + capacity: 1000, + pic: { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', + }, + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + kandang_id: 1, + payment_status: 'PAID', + created_user: adminUser, + created_at: '2025-01-15T10:00:00Z', + updated_at: '2025-01-15T10:00:00Z', + }, +]; + +export class ClosingApiService extends BaseApiService< + ClosingSales, + unknown, + unknown +> { + constructor(basePath: string = '') { + super(basePath); + } + + async getPenjualan( + id: number + ): Promise | undefined> { + try { + // TODO: Remove dummy data when real API is ready + await sleep(750); + + const saleData = DUMMY_SALES.find((sale) => sale.id === id); + + if (!saleData) { + return { + code: 404, + status: 'error', + message: 'Sales data not found!', + }; + } + + return { + code: 200, + status: 'success', + message: 'Successfully get sales data!', + meta: { + page: 1, + limit: 10, + total_pages: 1, + total_results: 1, + }, + data: saleData, + }; + + // const getPenjualanPath = `${this.basePath}/${id}/penjualan`; + // return await this.customRequest>(getPenjualanPath); + } catch (error) { + console.error('Error fetching penjualan:', error); + return undefined; + } + } +} + +export const ClosingApi = new ClosingApiService('/closing'); diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts new file mode 100644 index 00000000..d4b94770 --- /dev/null +++ b/src/types/api/closing/closing.d.ts @@ -0,0 +1,26 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { Kandang } from '@type/api/master-data/kandang'; +import { Product } from '@type/api/master-data/product'; +import { ProductCategory } from '@type/api/master-data/product-category'; +import { Customer } from '@type/api/master-data/customer'; + +export type BaseClosingSales = { + id: number; + realization_date: string; + week_age: number; + age_label: string; + delivery_order_number: string; + product: Product; + product_category: ProductCategory; + customer: Customer; + quantity: number; + weight: number; + average: number; + price: number; + total: number; + kandang: Kandang; + kandang_id: number; + payment_status: string; +}; + +export type ClosingSales = BaseMetadata & BaseClosingSales; From 647b002065d77bbe201975331978eb5089aa681f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 11:55:57 +0700 Subject: [PATCH 14/38] refactor(FE-326): change SalesReportTable to use BaseClosingSales type and support initial values --- .../pages/closing/sale/SalesReportTable.tsx | 147 ++---------------- 1 file changed, 15 insertions(+), 132 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 525d1dcf..85fe50a5 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -7,25 +7,12 @@ import Table, { CustomHeaderRow } from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; +import { BaseClosingSales } from '@/types/api/closing/closing'; -type BaseClosingSales = { - id: number; - realization_date: string; - week_age: number; - age_label: string; - delivery_order_number: string; - product: string; - product_type: string; - customer: string; - quantity: number; - weight: number; - average: number; - price: number; - total: number; - kandang: string; - kandang_id: number; - payment_status: string; -}; +interface SalesReportTableProps { + type?: 'detail'; + initialValues?: BaseClosingSales; +} const generateCustomHeaders = (template: { groups: Array<{ @@ -101,122 +88,18 @@ const generateCustomHeaders = (template: { return rows; }; -const SalesReportTable = () => { +const SalesReportTable = ({ + type = 'detail', + initialValues, +}: SalesReportTableProps) => { const [activeTabId, setActiveTabId] = useState('penjualan'); - const salesBroilerData: BaseClosingSales[] = useMemo( - () => [ - { - id: 1, - realization_date: '2025-10-02', - week_age: 3, - age_label: '3 Weeks', - delivery_order_number: 'DO.MBU.699', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'TIAN YUSTIAN', - quantity: 1045, - weight: 1000, - average: 0.96, - price: 25300, - total: 25300000, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 2, - realization_date: '2025-10-07', - week_age: 4, - age_label: '4 Weeks', - delivery_order_number: 'DO.MBU.1037', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'ZAENAL MUTAQIN', - quantity: 850, - weight: 1211.4, - average: 1.43, - price: 23700, - total: 28710180, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 3, - realization_date: '2025-10-09', - week_age: 4, - age_label: '4 Weeks', - delivery_order_number: 'DO.MBU.1107', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'CORNELIUS TONY KUSTANTO', - quantity: 560, - weight: 990, - average: 1.77, - price: 23100, - total: 22869000, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 4, - realization_date: '2025-10-09', - week_age: 0, - age_label: '', - delivery_order_number: 'DO.MBU.1108', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'CV. KOPO AB', - quantity: 1088, - weight: 1934.3, - average: 1.78, - price: 23100, - total: 44682330, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 5, - realization_date: '2025-10-09', - week_age: 0, - age_label: '', - delivery_order_number: 'DO.MBU.1110', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'H. MAMAN ROMANSAH', - quantity: 624, - weight: 1121.4, - average: 1.8, - price: 22960, - total: 25747344, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 6, - realization_date: '2025-10-09', - week_age: 0, - age_label: '', - delivery_order_number: 'DO.MBU.1133', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'PT. SAMUDERA MULIA LESTARI', - quantity: 624, - weight: 1102.3, - average: 1.77, - price: 23100, - total: 25463130, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - ], - [] - ); + const salesBroilerData: BaseClosingSales[] = useMemo(() => { + if (activeTabId === 'penjualan' && initialValues) { + return [initialValues]; + } + return []; + }, [initialValues, activeTabId]); const totals = useMemo(() => { const totalQuantity = salesBroilerData.reduce( From ba40bbb1d37c647f91cbfb567e86c9f28d388a26 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:07:10 +0700 Subject: [PATCH 15/38] refactor(FE-327): remove dummy sales data and update ClosingApiService to fetch real sales data from API --- src/services/api/closing.ts | 149 ++---------------------------------- 1 file changed, 7 insertions(+), 142 deletions(-) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 6dab6f8d..1378ab58 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -1,123 +1,13 @@ import { BaseApiService } from './base'; -import { BaseApiResponse, CreatedUser } from '@/types/api/api-general'; +import { BaseApiResponse } from '@/types/api/api-general'; import { ClosingSales } from '@/types/api/closing/closing'; -import { sleep } from '@/lib/helper'; - -const adminUser: CreatedUser = { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin User', -}; - -const DUMMY_SALES: ClosingSales[] = [ - { - id: 1, - realization_date: '2025-01-15', - week_age: 4, - age_label: 'Week 4', - delivery_order_number: 'DO-2025-001', - product: { - id: 1, - name: 'Ayam Broiler', - brand: 'Brand A', - sku: 'AYM-001', - product_price: 20000, - selling_price: 25000, - tax: 10, - expiry_period: 30, - uom: { - id: 1, - name: 'Kg', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - product_category: { - id: 1, - code: 'LC001', - name: 'Live Chicken', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - suppliers: [], - flags: [], - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - product_category: { - id: 1, - code: 'LC001', - name: 'Live Chicken', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - customer: { - id: 1, - name: 'PT. Bumi Mandiri', - pic_id: 1, - pic: adminUser, - type: 'COMPANY', - address: 'Jl. Industri No. 123', - phone: '+62-21-1234567', - email: 'info@bumimandiri.com', - account_number: '1234567890', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - quantity: 1000, - weight: 1850, - average: 1.85, - price: 25000, - total: 25000000, - kandang: { - id: 1, - name: 'Singaparna 1', - status: 'ACTIVE', - location: { - id: 1, - name: 'Singaparna', - address: 'Jl. Singaparna No. 1', - area: { - id: 1, - name: 'Tasikmalaya', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - capacity: 1000, - pic: { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin User', - }, - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - kandang_id: 1, - payment_status: 'PAID', - created_user: adminUser, - created_at: '2025-01-15T10:00:00Z', - updated_at: '2025-01-15T10:00:00Z', - }, -]; export class ClosingApiService extends BaseApiService< ClosingSales, unknown, unknown > { - constructor(basePath: string = '') { + constructor(basePath: string) { super(basePath); } @@ -125,36 +15,11 @@ export class ClosingApiService extends BaseApiService< id: number ): Promise | undefined> { try { - // TODO: Remove dummy data when real API is ready - await sleep(750); - - const saleData = DUMMY_SALES.find((sale) => sale.id === id); - - if (!saleData) { - return { - code: 404, - status: 'error', - message: 'Sales data not found!', - }; - } - - return { - code: 200, - status: 'success', - message: 'Successfully get sales data!', - meta: { - page: 1, - limit: 10, - total_pages: 1, - total_results: 1, - }, - data: saleData, - }; - - // const getPenjualanPath = `${this.basePath}/${id}/penjualan`; - // return await this.customRequest>(getPenjualanPath); - } catch (error) { - console.error('Error fetching penjualan:', error); + const getPenjualanPath = `http://localhost:4010/api/closing/${id}/penjualan`; + return await this.customRequest>( + getPenjualanPath + ); + } catch { return undefined; } } From 1a4a05308f0ee94f926af968954b702f077969f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:08:44 +0700 Subject: [PATCH 16/38] refactor(FE-326,327): enhance SalesReportTable to handle empty sales data and conditionally render summary row --- .../pages/closing/sale/SalesReportTable.tsx | 112 ++++++++++-------- 1 file changed, 63 insertions(+), 49 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 85fe50a5..18af45a5 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -102,6 +102,18 @@ const SalesReportTable = ({ }, [initialValues, activeTabId]); const totals = useMemo(() => { + if (salesBroilerData.length === 0) { + return { + totalQuantity: 0, + totalWeight: 0, + avgWeight: 0, + avgPricePartner: 0, + totalPartner: 0, + avgPriceAct: 0, + totalAct: 0, + }; + } + const totalQuantity = salesBroilerData.reduce( (sum, item) => sum + (item.quantity || 0), 0 @@ -113,7 +125,7 @@ const SalesReportTable = ({ const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; const validPriceItems = salesBroilerData.filter( - (item) => item.price != null + (item) => item.price != null && item.price > 0 ); const avgPricePartner = validPriceItems.length > 0 @@ -374,54 +386,56 @@ const SalesReportTable = ({ /> {/* Summary Row */} - - - - - - - - - - - - - - - - - - - -
- Total Penjualan - - - - - - - - - - - - - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPriceAct)} - - {formatCurrency(totals.totalAct)} - - - - - - -
+ {salesBroilerData.length > 0 && ( + + + + + + + + + + + + + + + + + + + +
+ Total Penjualan + + - + + - + + - + + - + + {formatNumber(totals.totalQuantity)} + + {formatNumber(totals.totalWeight)} + + {formatNumber(totals.avgWeight)} + + {formatCurrency(totals.avgPricePartner)} + + {formatCurrency(totals.totalPartner)} + + {formatCurrency(totals.avgPriceAct)} + + {formatCurrency(totals.totalAct)} + + - + + - +
+ )} ), From 8ea29579ecb5d712856f065dc615ca3e2e51b84d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:10:57 +0700 Subject: [PATCH 17/38] feat(FE-326,327): add layout and closing detail page with sales report integration --- src/app/closing/detail/layout.tsx | 11 +++++++ src/app/closing/detail/page.tsx | 55 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/app/closing/detail/layout.tsx create mode 100644 src/app/closing/detail/page.tsx diff --git a/src/app/closing/detail/layout.tsx b/src/app/closing/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/closing/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx new file mode 100644 index 00000000..43a8469a --- /dev/null +++ b/src/app/closing/detail/page.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; +import { ClosingApi } from '@/services/api/closing'; +import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; + +const ClosingDetailPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const closingId = searchParams.get('closingId'); + + const { data: closing, isLoading: isLoadingClosing } = useSWR( + closingId, + (id: string) => { + const numericId = parseInt(id, 10); + if (isNaN(numericId) || numericId <= 0) { + throw new Error('Invalid closing ID'); + } + return ClosingApi.getPenjualan(numericId); + } + ); + + if (!closingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingClosing && (!closing || isResponseError(closing))) { + // router.replace('/404'); + return; + } + + return ( +
+ {isLoadingClosing && ( +
+ +
+ )} + {!isLoadingClosing && isResponseSuccess(closing) && ( + + )} +
+ ); +}; + +export default ClosingDetailPage; From 492efb18e24506d56ff7c948eedc48e62ed9b544 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:15:58 +0700 Subject: [PATCH 18/38] chore: update next.js to version 15.5.7 in package.json and package-lock.json --- package-lock.json | 92 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec1316ae..535bb986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "^15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1082,9 +1082,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1098,9 +1098,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -1114,9 +1114,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -1146,9 +1146,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -1162,9 +1162,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -1178,9 +1178,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -1194,9 +1194,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -1210,9 +1210,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -1855,6 +1855,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1924,6 +1925,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2447,6 +2449,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3060,7 +3063,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.3.10", @@ -3516,6 +3520,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3689,6 +3694,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,12 +5660,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.3", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -5672,14 +5678,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.3", - "@next/swc-darwin-x64": "15.5.3", - "@next/swc-linux-arm64-gnu": "15.5.3", - "@next/swc-linux-arm64-musl": "15.5.3", - "@next/swc-linux-x64-gnu": "15.5.3", - "@next/swc-linux-x64-musl": "15.5.3", - "@next/swc-win32-arm64-msvc": "15.5.3", - "@next/swc-win32-x64-msvc": "15.5.3", + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { @@ -6167,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6197,6 +6204,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7083,6 +7091,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7250,6 +7259,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 7396d49d..85485ee3 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "^15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", From 15ced14e20b9c9bca49ae2d291f736a173b4e1b6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:25:11 +0700 Subject: [PATCH 19/38] refactor(FE-Storyless): add footer rendering support to Table component --- src/components/Table.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index eafd3e7a..96f23e8e 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -75,6 +75,8 @@ export interface TableProps { column: Column, headerGroup: HeaderGroup ) => ReactNode; + renderFooter?: boolean; + footerContent?: ReactNode; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -121,6 +123,8 @@ const Table = ({ customHeaderRows = [], renderCustomHeaders = false, onCustomHeaderCellRender, + renderFooter = false, + footerContent, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -327,6 +331,8 @@ const Table = ({ ))} + + {renderFooter && footerContent} From 949761d59d4b98faa6331321fbe07cee9a6c6744 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:25:27 +0700 Subject: [PATCH 20/38] feat(FE-326,327): add footer rendering to SalesReportTable for total sales display --- .../pages/closing/sale/SalesReportTable.tsx | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 18af45a5..552985a9 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -375,20 +375,9 @@ const SalesReportTable = ({ columns={salesColumns} renderCustomHeaders={true} customHeaderRows={salesCustomHeaderRows} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'hidden', - bodyRowClassName: 'hover:bg-gray-50 transition-colors', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300', - }} - /> - - {/* Summary Row */} - {salesBroilerData.length > 0 && ( - - + renderFooter={salesBroilerData.length > 0} + footerContent={ + - -
Total Penjualan @@ -433,9 +422,17 @@ const SalesReportTable = ({ -
- )} + + } + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'hidden', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300', + }} + /> ), From b095208fae29059050f25be05131dd05040142ad Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 17:41:22 +0700 Subject: [PATCH 21/38] refactor(FE-327): Temporarily map Indonesian sales fields to English --- .../pages/closing/sale/SalesReportTable.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 552985a9..b702752a 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -88,6 +88,34 @@ const generateCustomHeaders = (template: { return rows; }; +// TODO: TEMPORARY - Remove this when backend API returns English field names +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { + if (!data || !data.penjualan || !Array.isArray(data.penjualan)) { + return []; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return data.penjualan.map((item: any) => ({ + id: item.id, + realization_date: item.tanggal_realisasi, + age_label: item.umur_label, + umur_minggu: item.umur_minggu, + delivery_order_number: item.no_do, + product: item.produk, + jenis_produk: item.jenis_produk, + customer: item.customer, + quantity: item.qty, + weight: item.kg, + average: item.avg, + price: item.harga, + total: item.total, + kandang: item.kandang, + payment_status: item.status_pembayaran, + })); +}; +// END TODO + const SalesReportTable = ({ type = 'detail', initialValues, @@ -96,6 +124,13 @@ const SalesReportTable = ({ const salesBroilerData: BaseClosingSales[] = useMemo(() => { if (activeTabId === 'penjualan' && initialValues) { + // TODO: TEMPORARY - Remove this when backend API returns English field names + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) { + return mapIndonesianDataToEnglish(initialValues); + } + // END TODO return [initialValues]; } return []; From 7d9a88cf3b8dc293ba71c671036b83b9aedc5086 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 20:14:29 +0700 Subject: [PATCH 22/38] feat(FE-326,327): Add sortable table headers and styling --- src/components/Table.tsx | 76 +++++++++++++--- .../pages/closing/sale/SalesReportTable.tsx | 88 +++++++++++-------- 2 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 96f23e8e..69406220 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -46,6 +46,7 @@ export interface CustomHeaderRow { colSpan?: number; rowSpan?: number; className?: string; + field?: string; }>; className?: string; } @@ -236,18 +237,69 @@ const Table = ({ headerRow.className || className.customHeaderRowClassName } > - {headerRow.cells.map((cell) => ( - - {cell.content} - - ))} + {headerRow.cells.map((cell) => { + const column = table + .getAllColumns() + .find((col) => col.id === cell.field); + + const canSort = column?.getCanSort(); + const sortingState = column?.getIsSorted(); + + return ( + +
+ {cell.content} + + {canSort && ( +
+ + +
+ )} +
+ + ); + })} ))} diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index b702752a..b9f73635 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -14,6 +14,15 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } +interface HeaderCell { + id: string; + content: React.ReactNode; + colSpan?: number; + rowSpan?: number; + className: string; + field?: string; +} + const generateCustomHeaders = (template: { groups: Array<{ label: string; @@ -41,31 +50,43 @@ const generateCustomHeaders = (template: { template.groups.forEach((group) => { if (group.subLabels) { - mainRow.push({ + const mainCell: HeaderCell = { id: `${group.field || 'group'}-${subColumnIndex}`, content: group.label, colSpan: group.colSpan, className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-300', - }); + 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-200', + }; + + mainRow.push(mainCell); group.subLabels.forEach((subLabel) => { - subRow.push({ + const subCell: HeaderCell = { id: `sub-${subColumnIndex}`, content: subLabel, className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300 border-t-0', - }); + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', + }; + + if (group.label === 'Jumlah') { + subCell.field = subLabel === 'Ekor' ? 'quantity' : 'weight'; + } + + subRow.push(subCell); subColumnIndex++; }); } else { - mainRow.push({ + const mainCell: HeaderCell = { id: `${group.field}-header`, content: group.label, rowSpan: group.rowSpan, className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300', - }); + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', + }; + + mainCell.field = group.field; + + mainRow.push(mainCell); } }); @@ -371,7 +392,7 @@ const SalesReportTable = ({ }, { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, - { label: 'Total Mitra (Rp)', field: 'total_partner', rowSpan: 2 }, + { label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 }, { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, { label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 }, { label: 'Kandang', field: 'kandang', rowSpan: 2 }, @@ -413,49 +434,37 @@ const SalesReportTable = ({ renderFooter={salesBroilerData.length > 0} footerContent={ - - + + Total Penjualan - - - - - - - - - - - - - - - - - + + + + + {formatNumber(totals.totalQuantity)} - + {formatNumber(totals.totalWeight)} - + {formatNumber(totals.avgWeight)} - + {formatCurrency(totals.avgPricePartner)} - + {formatCurrency(totals.totalPartner)} - + {formatCurrency(totals.avgPriceAct)} - + {formatCurrency(totals.totalAct)} - - - - - - - - + + } @@ -463,9 +472,10 @@ const SalesReportTable = ({ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'hidden', - bodyRowClassName: 'hover:bg-gray-50 transition-colors', + 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 border border-gray-300', + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> From 075d945a59a193d33f56b8e3d213592fcd7e0dac Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 21:29:45 +0700 Subject: [PATCH 23/38] refactor(FE-326): Use placeholder for sales type in header --- src/components/pages/closing/sale/SalesReportTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index b9f73635..ca616107 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -419,7 +419,7 @@ const SalesReportTable = ({ content: (

- Penjualan Ayam Besar + Penjualan {'(diisi dengan jenis penjualan)'}

Date: Fri, 5 Dec 2025 11:14:52 +0700 Subject: [PATCH 24/38] refactor(FE-327): Split BaseClosingSales into BaseSales and wrapper --- src/types/api/closing/closing.d.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index d4b94770..6b17d8e1 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -4,7 +4,7 @@ import { Product } from '@type/api/master-data/product'; import { ProductCategory } from '@type/api/master-data/product-category'; import { Customer } from '@type/api/master-data/customer'; -export type BaseClosingSales = { +export type BaseSales = { id: number; realization_date: string; week_age: number; @@ -23,4 +23,9 @@ export type BaseClosingSales = { payment_status: string; }; +export type BaseClosingSales = { + project_type: string; + penjualan: BaseSales[]; +}; + export type ClosingSales = BaseMetadata & BaseClosingSales; From 46e072bbcfaf091a9c0fafe30c3504dd95524c7f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 11:15:41 +0700 Subject: [PATCH 25/38] refactor(FE-327): Map Indonesian sales fields and add API sample --- .../pages/closing/sale/SalesReportTable.tsx | 94 ++++++++++++++++--- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index ca616107..d93dbf2e 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -7,7 +7,7 @@ import Table, { CustomHeaderRow } from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; -import { BaseClosingSales } from '@/types/api/closing/closing'; +import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; interface SalesReportTableProps { type?: 'detail'; @@ -110,8 +110,7 @@ const generateCustomHeaders = (template: { }; // TODO: TEMPORARY - Remove this when backend API returns English field names -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { +const mapIndonesianDataToEnglish = (data: BaseClosingSales): BaseSales[] => { if (!data || !data.penjualan || !Array.isArray(data.penjualan)) { return []; } @@ -120,11 +119,11 @@ const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { return data.penjualan.map((item: any) => ({ id: item.id, realization_date: item.tanggal_realisasi, + week_age: item.umur_minggu, age_label: item.umur_label, - umur_minggu: item.umur_minggu, delivery_order_number: item.no_do, product: item.produk, - jenis_produk: item.jenis_produk, + product_category: item.jenis_produk, customer: item.customer, quantity: item.qty, weight: item.kg, @@ -132,6 +131,7 @@ const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { price: item.harga, total: item.total, kandang: item.kandang, + kandang_id: item.kandang_id, payment_status: item.status_pembayaran, })); }; @@ -143,16 +143,14 @@ const SalesReportTable = ({ }: SalesReportTableProps) => { const [activeTabId, setActiveTabId] = useState('penjualan'); - const salesBroilerData: BaseClosingSales[] = useMemo(() => { + const salesBroilerData: BaseSales[] = useMemo(() => { if (activeTabId === 'penjualan' && initialValues) { // TODO: TEMPORARY - Remove this when backend API returns English field names - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) { return mapIndonesianDataToEnglish(initialValues); } // END TODO - return [initialValues]; + return []; } return []; }, [initialValues, activeTabId]); @@ -208,7 +206,7 @@ const SalesReportTable = ({ }; }, [salesBroilerData]); - const salesColumns: ColumnDef[] = useMemo( + const salesColumns: ColumnDef[] = useMemo( () => [ { id: 'realization_date', @@ -418,9 +416,7 @@ const SalesReportTable = ({ label: 'Penjualan', content: (
-

- Penjualan {'(diisi dengan jenis penjualan)'} -

+

Penjualan

+
+ +
+            {JSON.stringify(
+              {
+                code: 200,
+                status: 'success',
+                message: 'Retrieved sales report successfully',
+                data: {
+                  project_type: 'GROWING',
+                  flock_id: '1',
+                  period: 10,
+                  sales: [
+                    {
+                      id: 1,
+                      realization_date: '2025-12-05T02:22:17.443165Z',
+                      age: 20,
+                      do_number: 'SO-DO-10001',
+                      product: {
+                        id: 1,
+                        name: 'Laptop Gaming X500',
+                        product_price: 15000000,
+                        selling_price: 16500000.5,
+                        uom: {
+                          id: 1,
+                          name: 'KG',
+                        },
+                        flags: ['Best Seller', 'New Arrival'],
+                        product_category: {
+                          id: 5,
+                          name: 'Elektronik',
+                          code: 'DOC',
+                        },
+                      },
+                      customer: {
+                        id: 12345,
+                        name: 'PT. Solusi Teknologi Nusantara',
+                        type: 'Perusahaan',
+                        account_number: 'ACC1234567890',
+                        balance: 5000000.75,
+                        pic: {
+                          id: 101,
+                          name: 'Budi Santoso',
+                          email: 'budi.santoso@example.com',
+                          role: 'Manajer Akun',
+                        },
+                      },
+                      qty: 6348,
+                      weight: 19142,
+                      avg_weight: 3.02,
+                      price: 26419,
+                      total_price: 505712498,
+                      kandang: {
+                        id: 1,
+                        name: 'cibeber 1',
+                      },
+                      payment_status: 'Paid',
+                    },
+                  ],
+                },
+              },
+              null,
+              2
+            )}
+          
+
+
); }; From f205c6650985f7d3af07f5a127e18d2564815c9c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 17:49:59 +0700 Subject: [PATCH 26/38] refactor(FE-327): Rename Ekor label to Kuantitas --- src/components/pages/closing/sale/SalesReportTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index d93dbf2e..6796465e 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -69,7 +69,7 @@ const generateCustomHeaders = (template: { }; if (group.label === 'Jumlah') { - subCell.field = subLabel === 'Ekor' ? 'quantity' : 'weight'; + subCell.field = subLabel === 'Kuantitas' ? 'quantity' : 'weight'; } subRow.push(subCell); @@ -244,7 +244,7 @@ const SalesReportTable = ({ { id: 'quantity', accessorKey: 'quantity', - header: 'Ekor', + header: 'Kuantitas', cell: (props) => { const value = props.getValue() as number; const isSummary = props.row.id === 'summary'; @@ -386,7 +386,7 @@ const SalesReportTable = ({ { label: 'Jumlah', colSpan: 2, - subLabels: ['Ekor', 'Kg'], + subLabels: ['Kuantitas', 'Kg'], }, { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, From 5869e0434b175f859a9b3e4fd753c4ac55f98bda Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 18:26:58 +0700 Subject: [PATCH 27/38] refactor(FE-327): change closing API paths and sales types --- src/services/api/closing.ts | 4 ++-- src/types/api/closing/closing.d.ts | 20 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 1378ab58..66f88c76 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -15,7 +15,7 @@ export class ClosingApiService extends BaseApiService< id: number ): Promise | undefined> { try { - const getPenjualanPath = `http://localhost:4010/api/closing/${id}/penjualan`; + const getPenjualanPath = `${id}/penjualan`; return await this.customRequest>( getPenjualanPath ); @@ -25,4 +25,4 @@ export class ClosingApiService extends BaseApiService< } } -export const ClosingApi = new ClosingApiService('/closing'); +export const ClosingApi = new ClosingApiService('/closings'); diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index 6b17d8e1..03217438 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -1,31 +1,27 @@ import { BaseMetadata } from '@/types/api/api-general'; -import { Kandang } from '@type/api/master-data/kandang'; import { Product } from '@type/api/master-data/product'; -import { ProductCategory } from '@type/api/master-data/product-category'; import { Customer } from '@type/api/master-data/customer'; export type BaseSales = { id: number; realization_date: string; - week_age: number; - age_label: string; - delivery_order_number: string; + age: number; + do_number: string; product: Product; - product_category: ProductCategory; customer: Customer; - quantity: number; + qty: number; weight: number; - average: number; + avg_weight: number; price: number; - total: number; - kandang: Kandang; - kandang_id: number; + total_price: number; payment_status: string; }; export type BaseClosingSales = { project_type: string; - penjualan: BaseSales[]; + flock_id: number; + period: number; + sales: BaseSales[]; }; export type ClosingSales = BaseMetadata & BaseClosingSales; From 30db7ee95d806ed47785c1987b0ca43841b5ea51 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 18:27:45 +0700 Subject: [PATCH 28/38] refactor(FE-327): change SalesReportTable to use new API fields --- .../pages/closing/sale/SalesReportTable.tsx | 181 ++++-------------- 1 file changed, 32 insertions(+), 149 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 6796465e..64247890 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -8,6 +8,8 @@ import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; +import { Product } from '@type/api/master-data/product'; +import { Customer } from '@type/api/master-data/customer'; interface SalesReportTableProps { type?: 'detail'; @@ -109,34 +111,6 @@ const generateCustomHeaders = (template: { return rows; }; -// TODO: TEMPORARY - Remove this when backend API returns English field names -const mapIndonesianDataToEnglish = (data: BaseClosingSales): BaseSales[] => { - if (!data || !data.penjualan || !Array.isArray(data.penjualan)) { - return []; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return data.penjualan.map((item: any) => ({ - id: item.id, - realization_date: item.tanggal_realisasi, - week_age: item.umur_minggu, - age_label: item.umur_label, - delivery_order_number: item.no_do, - product: item.produk, - product_category: item.jenis_produk, - customer: item.customer, - quantity: item.qty, - weight: item.kg, - average: item.avg, - price: item.harga, - total: item.total, - kandang: item.kandang, - kandang_id: item.kandang_id, - payment_status: item.status_pembayaran, - })); -}; -// END TODO - const SalesReportTable = ({ type = 'detail', initialValues, @@ -144,13 +118,8 @@ const SalesReportTable = ({ const [activeTabId, setActiveTabId] = useState('penjualan'); const salesBroilerData: BaseSales[] = useMemo(() => { - if (activeTabId === 'penjualan' && initialValues) { - // TODO: TEMPORARY - Remove this when backend API returns English field names - if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) { - return mapIndonesianDataToEnglish(initialValues); - } - // END TODO - return []; + if (activeTabId === 'penjualan' && initialValues && initialValues.sales) { + return initialValues.sales; } return []; }, [initialValues, activeTabId]); @@ -169,7 +138,7 @@ const SalesReportTable = ({ } const totalQuantity = salesBroilerData.reduce( - (sum, item) => sum + (item.quantity || 0), + (sum, item) => sum + (item.qty || 0), 0 ); const totalWeight = salesBroilerData.reduce( @@ -188,7 +157,7 @@ const SalesReportTable = ({ : 0; const totalPartner = salesBroilerData.reduce( - (sum, item) => sum + (item.total || 0), + (sum, item) => sum + (item.total_price || 0), 0 ); @@ -218,14 +187,14 @@ const SalesReportTable = ({ }, }, { - id: 'age_label', - accessorKey: 'age_label', + id: 'age', + accessorKey: 'age', header: 'Umur', cell: (props) => props.getValue() || '-', }, { - id: 'delivery_order_number', - accessorKey: 'delivery_order_number', + id: 'do_number', + accessorKey: 'do_number', header: 'No. DO', cell: (props) => props.getValue() || '-', }, @@ -233,17 +202,23 @@ const SalesReportTable = ({ id: 'product', accessorKey: 'product', header: 'Produk', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const product = props.getValue() as Product; + return product?.name || '-'; + }, }, { id: 'customer', accessorKey: 'customer', header: 'Customer', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const customer = props.getValue() as Customer; + return customer?.name || '-'; + }, }, { - id: 'quantity', - accessorKey: 'quantity', + id: 'qty', + accessorKey: 'qty', header: 'Kuantitas', cell: (props) => { const value = props.getValue() as number; @@ -270,8 +245,8 @@ const SalesReportTable = ({ }, }, { - id: 'average', - accessorKey: 'average', + id: 'avg_weight', + accessorKey: 'avg_weight', header: 'AVG (Kg)', cell: (props) => { const value = props.getValue() as number; @@ -289,26 +264,16 @@ const SalesReportTable = ({ header: 'Harga Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { id: 'total_mitra', - accessorKey: 'total', + accessorKey: 'total_price', header: 'Total Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -317,26 +282,16 @@ const SalesReportTable = ({ header: 'Harga Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { id: 'total_act', - accessorKey: 'total', + accessorKey: 'total_price', header: 'Total Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -379,16 +334,16 @@ const SalesReportTable = ({ const headerTemplate = { groups: [ { label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 }, - { label: 'Umur', field: 'age_label', rowSpan: 2 }, - { label: 'No. DO', field: 'delivery_order_number', rowSpan: 2 }, + { label: 'Umur', field: 'age', rowSpan: 2 }, + { label: 'No. DO', field: 'do_number', rowSpan: 2 }, { label: 'Produk', field: 'product', rowSpan: 2 }, { label: 'Customer', field: 'customer', rowSpan: 2 }, { label: 'Jumlah', colSpan: 2, - subLabels: ['Kuantitas', 'Kg'], + subLabels: ['Qty', 'Kg'], }, - { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, + { label: 'AVG (Kg)', field: 'avg_weight', rowSpan: 2 }, { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, { label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 }, { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, @@ -482,78 +437,6 @@ const SalesReportTable = ({ variant='lifted' /> -
- -
-            {JSON.stringify(
-              {
-                code: 200,
-                status: 'success',
-                message: 'Retrieved sales report successfully',
-                data: {
-                  project_type: 'GROWING',
-                  flock_id: '1',
-                  period: 10,
-                  sales: [
-                    {
-                      id: 1,
-                      realization_date: '2025-12-05T02:22:17.443165Z',
-                      age: 20,
-                      do_number: 'SO-DO-10001',
-                      product: {
-                        id: 1,
-                        name: 'Laptop Gaming X500',
-                        product_price: 15000000,
-                        selling_price: 16500000.5,
-                        uom: {
-                          id: 1,
-                          name: 'KG',
-                        },
-                        flags: ['Best Seller', 'New Arrival'],
-                        product_category: {
-                          id: 5,
-                          name: 'Elektronik',
-                          code: 'DOC',
-                        },
-                      },
-                      customer: {
-                        id: 12345,
-                        name: 'PT. Solusi Teknologi Nusantara',
-                        type: 'Perusahaan',
-                        account_number: 'ACC1234567890',
-                        balance: 5000000.75,
-                        pic: {
-                          id: 101,
-                          name: 'Budi Santoso',
-                          email: 'budi.santoso@example.com',
-                          role: 'Manajer Akun',
-                        },
-                      },
-                      qty: 6348,
-                      weight: 19142,
-                      avg_weight: 3.02,
-                      price: 26419,
-                      total_price: 505712498,
-                      kandang: {
-                        id: 1,
-                        name: 'cibeber 1',
-                      },
-                      payment_status: 'Paid',
-                    },
-                  ],
-                },
-              },
-              null,
-              2
-            )}
-          
-
-
); }; From eaf118845c4af32c0f3962ee60de20c7e5276f7f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 19:15:38 +0700 Subject: [PATCH 29/38] feat(FE-327): Include Kandang in sales data and display name --- src/components/pages/closing/sale/SalesReportTable.tsx | 6 +++++- src/types/api/closing/closing.d.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 64247890..e2956bb6 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -10,6 +10,7 @@ import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; import { Product } from '@type/api/master-data/product'; import { Customer } from '@type/api/master-data/customer'; +import { Kandang } from '@type/api/master-data/kandang'; interface SalesReportTableProps { type?: 'detail'; @@ -298,7 +299,10 @@ const SalesReportTable = ({ id: 'kandang', accessorKey: 'kandang', header: 'Kandang', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const kandang = props.getValue() as Kandang; + return kandang?.name || '-'; + }, }, { id: 'payment_status', diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index 03217438..64d0d465 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -1,6 +1,7 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Product } from '@type/api/master-data/product'; import { Customer } from '@type/api/master-data/customer'; +import { Kandang } from '@type/api/master-data/kandang'; export type BaseSales = { id: number; @@ -14,6 +15,7 @@ export type BaseSales = { avg_weight: number; price: number; total_price: number; + kandang: Kandang; payment_status: string; }; From 4fe53f364a1756a1b91e3d89b3b4955e52907476 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 08:54:12 +0700 Subject: [PATCH 30/38] refactor(FE-326): Remove Tabs wrapper from SalesReportTable --- .../pages/closing/sale/SalesReportTable.tsx | 138 ++++++++---------- 1 file changed, 62 insertions(+), 76 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index e2956bb6..7213a621 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -1,6 +1,5 @@ 'use client'; -import Tabs from '@/components/Tabs'; import React, { useState, useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; import Table, { CustomHeaderRow } from '@/components/Table'; @@ -365,81 +364,68 @@ const SalesReportTable = ({ return ( <>
- -

Penjualan

- - 0} - footerContent={ - - - - - - - - - - - - - - - - - - - } - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'hidden', - 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', - }} - /> - - - ), - }, - ]} - variant='lifted' - /> +
+

Penjualan

+ +
- Total Penjualan - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPriceAct)} - - {formatCurrency(totals.totalAct)} -
0} + footerContent={ + + + + + + + + + + + + + + + + + + + } + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'hidden', + 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', + }} + /> + + ); From 4ff16499918b0483ebd0c48d19da14a88392fa96 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 08:56:14 +0700 Subject: [PATCH 31/38] chore(FE-327): Remove unused state from SalesReportTable --- .../pages/closing/sale/SalesReportTable.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 7213a621..a62c20a6 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; import Table, { CustomHeaderRow } from '@/components/Table'; import Card from '@/components/Card'; @@ -115,14 +115,9 @@ const SalesReportTable = ({ type = 'detail', initialValues, }: SalesReportTableProps) => { - const [activeTabId, setActiveTabId] = useState('penjualan'); - const salesBroilerData: BaseSales[] = useMemo(() => { - if (activeTabId === 'penjualan' && initialValues && initialValues.sales) { - return initialValues.sales; - } - return []; - }, [initialValues, activeTabId]); + return initialValues?.sales || []; + }, [initialValues]); const totals = useMemo(() => { if (salesBroilerData.length === 0) { From ff1493b520b00b4e4e35320b9add9154bf4c5816 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:09:41 +0700 Subject: [PATCH 32/38] refactor(FE-326): Remove avgPriceAct/totalAct and use partner totals, fix badge case --- .../pages/closing/sale/SalesReportTable.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index a62c20a6..dedcb498 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -127,8 +127,6 @@ const SalesReportTable = ({ avgWeight: 0, avgPricePartner: 0, totalPartner: 0, - avgPriceAct: 0, - totalAct: 0, }; } @@ -156,17 +154,12 @@ const SalesReportTable = ({ 0 ); - const avgPriceAct = avgPricePartner; - const totalAct = totalPartner; - return { totalQuantity, totalWeight, avgWeight, avgPricePartner, totalPartner, - avgPriceAct, - totalAct, }; }, [salesBroilerData]); @@ -307,12 +300,10 @@ const SalesReportTable = ({ const getStatusColor = (status: string) => { if (!status) return 'neutral'; switch (status.toLowerCase()) { - case 'lunas': + case 'paid': return 'success'; - case 'pending': + case 'tempo': return 'warning'; - case 'belum lunas': - return 'error'; default: return 'neutral'; } @@ -399,10 +390,10 @@ const SalesReportTable = ({ {formatCurrency(totals.totalPartner)} From aad24c3c58e97527b95e83bb212befe74de3ba54 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:12:02 +0700 Subject: [PATCH 33/38] refactor(FE-327): Rename salesBroilerData to salesData --- .../pages/closing/sale/SalesReportTable.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index dedcb498..a473dd14 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -115,12 +115,12 @@ const SalesReportTable = ({ type = 'detail', initialValues, }: SalesReportTableProps) => { - const salesBroilerData: BaseSales[] = useMemo(() => { + const salesData: BaseSales[] = useMemo(() => { return initialValues?.sales || []; }, [initialValues]); const totals = useMemo(() => { - if (salesBroilerData.length === 0) { + if (salesData.length === 0) { return { totalQuantity: 0, totalWeight: 0, @@ -130,17 +130,17 @@ const SalesReportTable = ({ }; } - const totalQuantity = salesBroilerData.reduce( + const totalQuantity = salesData.reduce( (sum, item) => sum + (item.qty || 0), 0 ); - const totalWeight = salesBroilerData.reduce( + const totalWeight = salesData.reduce( (sum, item) => sum + (item.weight || 0), 0 ); const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; - const validPriceItems = salesBroilerData.filter( + const validPriceItems = salesData.filter( (item) => item.price != null && item.price > 0 ); const avgPricePartner = @@ -149,7 +149,7 @@ const SalesReportTable = ({ validPriceItems.length : 0; - const totalPartner = salesBroilerData.reduce( + const totalPartner = salesData.reduce( (sum, item) => sum + (item.total_price || 0), 0 ); @@ -161,7 +161,7 @@ const SalesReportTable = ({ avgPricePartner, totalPartner, }; - }, [salesBroilerData]); + }, [salesData]); const salesColumns: ColumnDef[] = useMemo( () => [ @@ -359,11 +359,11 @@ const SalesReportTable = ({ }} >
+ Total Penjualan + + {formatNumber(totals.totalQuantity)} + + {formatNumber(totals.totalWeight)} + + {formatNumber(totals.avgWeight)} + + {formatCurrency(totals.avgPricePartner)} + + {formatCurrency(totals.totalPartner)} + + {formatCurrency(totals.avgPriceAct)} + + {formatCurrency(totals.totalAct)} +
- {formatCurrency(totals.avgPriceAct)} + {formatCurrency(totals.avgPricePartner)} - {formatCurrency(totals.totalAct)} + {formatCurrency(totals.totalPartner)}
0} + renderFooter={salesData.length > 0} footerContent={ From c9552dec2db111b47ed73f66fa46987d19f87ea7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:47:38 +0700 Subject: [PATCH 34/38] refactor(FE-326): Remove custom header rows and simplify Table --- src/components/Table.tsx | 208 ++++-------------- .../pages/closing/sale/SalesReportTable.tsx | 138 +----------- 2 files changed, 53 insertions(+), 293 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 69406220..b5148fea 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,8 +14,6 @@ import { SortingState, OnChangeFn, Row, - HeaderGroup, - Column, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -34,21 +32,6 @@ interface TableClassNames { bodyRowClassName?: string; bodyColumnClassName?: string; paginationClassName?: string; - customHeaderRowClassName?: string; - customHeaderCellClassName?: string; -} - -export interface CustomHeaderRow { - id: string; - cells: Array<{ - id: string; - content: ReactNode; - colSpan?: number; - rowSpan?: number; - className?: string; - field?: string; - }>; - className?: string; } export interface TableProps { @@ -69,13 +52,6 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); - customHeaderRows?: CustomHeaderRow[]; - renderCustomHeaders?: boolean; - onCustomHeaderCellRender?: ( - cell: ReactNode, - column: Column, - headerGroup: HeaderGroup - ) => ReactNode; renderFooter?: boolean; footerContent?: ReactNode; } @@ -111,8 +87,6 @@ const Table = ({ bodyRowClassName: '', bodyColumnClassName: '', paginationClassName: '', - customHeaderRowClassName: '', - customHeaderCellClassName: '', }, emptyContent = emptyContentDefaultValue, sorting, @@ -121,9 +95,6 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, - customHeaderRows = [], - renderCustomHeaders = false, - onCustomHeaderCellRender, renderFooter = false, footerContent, }: TableProps) => { @@ -228,143 +199,55 @@ const Table = ({
- {renderCustomHeaders && - customHeaderRows.length > 0 && - customHeaderRows.map((headerRow) => ( - - {headerRow.cells.map((cell) => { - const column = table - .getAllColumns() - .find((col) => col.id === cell.field); - - const canSort = column?.getCanSort(); - const sortingState = column?.getIsSorted(); - - return ( - - ); - })} - - ))} - {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => { - let cellContent = flexRender( - header.column.columnDef.header, - header.getContext() - ); - - if (onCustomHeaderCellRender) { - cellContent = onCustomHeaderCellRender( - cellContent, - header.column, - headerGroup - ); - } - - return ( - - ); - })} + {header.column.getCanSort() && ( +
+ + +
+ )} + + + ))} ))} @@ -383,7 +266,6 @@ const Table = ({ ))} - {renderFooter && footerContent}
-
- {cell.content} - - {canSort && ( -
- - -
- )} -
-
( + +
+ {flexRender( + header.column.columnDef.header, + header.getContext() )} - > -
- {cellContent} - {header.column.getCanSort() && ( -
- - -
- )} -
-
diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index a473dd14..3218d1d8 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; -import Table, { CustomHeaderRow } from '@/components/Table'; +import Table from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; @@ -16,101 +16,6 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } -interface HeaderCell { - id: string; - content: React.ReactNode; - colSpan?: number; - rowSpan?: number; - className: string; - field?: string; -} - -const generateCustomHeaders = (template: { - groups: Array<{ - label: string; - field?: string; - rowSpan?: number; - colSpan?: number; - subLabels?: string[]; - }>; -}): CustomHeaderRow[] => { - const mainRow: Array<{ - id: string; - content: React.ReactNode; - colSpan?: number; - rowSpan?: number; - className: string; - }> = []; - const subRow: Array<{ - id: string; - content: React.ReactNode; - colSpan?: number; - rowSpan?: number; - className: string; - }> = []; - let subColumnIndex = 0; - - template.groups.forEach((group) => { - if (group.subLabels) { - const mainCell: HeaderCell = { - id: `${group.field || 'group'}-${subColumnIndex}`, - content: group.label, - colSpan: group.colSpan, - className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-200', - }; - - mainRow.push(mainCell); - - group.subLabels.forEach((subLabel) => { - const subCell: HeaderCell = { - id: `sub-${subColumnIndex}`, - content: subLabel, - className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', - }; - - if (group.label === 'Jumlah') { - subCell.field = subLabel === 'Kuantitas' ? 'quantity' : 'weight'; - } - - subRow.push(subCell); - subColumnIndex++; - }); - } else { - const mainCell: HeaderCell = { - id: `${group.field}-header`, - content: group.label, - rowSpan: group.rowSpan, - className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', - }; - - mainCell.field = group.field; - - mainRow.push(mainCell); - } - }); - - const rows: CustomHeaderRow[] = [ - { - id: 'main-header', - cells: mainRow, - className: 'bg-gray-50', - }, - ]; - - if (subRow.length > 0) { - rows.push({ - id: 'sub-header', - cells: subRow, - className: 'bg-gray-50', - }); - } - - return rows; -}; - const SalesReportTable = ({ type = 'detail', initialValues, @@ -310,7 +215,7 @@ const SalesReportTable = ({ }; return ( - + {status || '-'} ); @@ -320,33 +225,6 @@ const SalesReportTable = ({ [] ); - const headerTemplate = { - groups: [ - { label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 }, - { label: 'Umur', field: 'age', rowSpan: 2 }, - { label: 'No. DO', field: 'do_number', rowSpan: 2 }, - { label: 'Produk', field: 'product', rowSpan: 2 }, - { label: 'Customer', field: 'customer', rowSpan: 2 }, - { - label: 'Jumlah', - colSpan: 2, - subLabels: ['Qty', 'Kg'], - }, - { label: 'AVG (Kg)', field: 'avg_weight', rowSpan: 2 }, - { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, - { label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 }, - { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, - { label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 }, - { label: 'Kandang', field: 'kandang', rowSpan: 2 }, - { label: 'Status Pembayaran', field: 'payment_status', rowSpan: 2 }, - ], - }; - - const salesCustomHeaderRows = useMemo( - () => generateCustomHeaders(headerTemplate), - [] - ); - return ( <>
@@ -361,8 +239,6 @@ const SalesReportTable = ({ 0} footerContent={ @@ -374,13 +250,13 @@ const SalesReportTable = ({ - - - ))} - {renderFooter && footerContent} + + {renderFooter && + (footerData && footerData.length > 0 + ? footerTable.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + : footerContent)} +
+ {formatNumber(totals.totalQuantity)} + {formatNumber(totals.totalWeight)} + {formatNumber(totals.avgWeight)} @@ -403,7 +279,9 @@ const SalesReportTable = ({ className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'hidden', + 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: From 27c867036f1d0ce3f1e9be7b0aeb935b2d973b99 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:51:40 +0700 Subject: [PATCH 35/38] refactor(FE-327): Update import paths for consistency in SalesReportTable --- src/components/pages/closing/sale/SalesReportTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 3218d1d8..1330b7f0 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -7,9 +7,9 @@ import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; -import { Product } from '@type/api/master-data/product'; -import { Customer } from '@type/api/master-data/customer'; -import { Kandang } from '@type/api/master-data/kandang'; +import { Product } from '@/types/api/master-data/product'; +import { Customer } from '@/types/api/master-data/customer'; +import { Kandang } from '@/types/api/master-data/kandang'; interface SalesReportTableProps { type?: 'detail'; From 99b9df27a78fa4f029615d5ff487b9cc1e72a22e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:58:38 +0700 Subject: [PATCH 36/38] refactor(FE-326): Comment _closing for copy-paste function --- src/app/{closing => _closing}/detail/layout.tsx | 0 src/app/{closing => _closing}/detail/page.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/app/{closing => _closing}/detail/layout.tsx (100%) rename src/app/{closing => _closing}/detail/page.tsx (97%) diff --git a/src/app/closing/detail/layout.tsx b/src/app/_closing/detail/layout.tsx similarity index 100% rename from src/app/closing/detail/layout.tsx rename to src/app/_closing/detail/layout.tsx diff --git a/src/app/closing/detail/page.tsx b/src/app/_closing/detail/page.tsx similarity index 97% rename from src/app/closing/detail/page.tsx rename to src/app/_closing/detail/page.tsx index 43a8469a..038e5072 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/_closing/detail/page.tsx @@ -34,7 +34,7 @@ const ClosingDetailPage = () => { } if (!isLoadingClosing && (!closing || isResponseError(closing))) { - // router.replace('/404'); + router.replace('/404'); return; } From e407410c4ab25c8e26f5eb4d0f055ba2331ea3a8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 10:25:40 +0700 Subject: [PATCH 37/38] feat(FE-Storyless): Add footer support to Table component --- src/components/Table.tsx | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b5148fea..5c76f44e 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -31,6 +31,9 @@ interface TableClassNames { tableBodyClassName?: string; bodyRowClassName?: string; bodyColumnClassName?: string; + tableFooterClassName?: string; + footerRowClassName?: string; + footerColumnClassName?: string; paginationClassName?: string; } @@ -54,6 +57,7 @@ export interface TableProps { enableRowSelection?: boolean | ((row: Row) => boolean); renderFooter?: boolean; footerContent?: ReactNode; + footerData?: TData[]; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -86,6 +90,9 @@ const Table = ({ tableBodyClassName: '', bodyRowClassName: '', bodyColumnClassName: '', + tableFooterClassName: '', + footerRowClassName: '', + footerColumnClassName: '', paginationClassName: '', }, emptyContent = emptyContentDefaultValue, @@ -97,6 +104,7 @@ const Table = ({ enableRowSelection, renderFooter = false, footerContent, + footerData = [], }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -164,6 +172,14 @@ const Table = ({ const table = useReactTable(tableOptions); const { setPageSize } = table; + const footerTableOptions: TableOptions = { + columns, + data: footerData, + getCoreRowModel: getCoreRowModel(), + }; + + const footerTable = useReactTable(footerTableOptions); + const prevPageClickHandler = () => { table.previousPage(); @@ -266,7 +282,26 @@ const Table = ({
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
From b3f7b8a3c530c0d782576e2d85f64d21e002fdf3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 10:26:26 +0700 Subject: [PATCH 38/38] feat(FE-326): Add totals footer row to sales report table --- .../pages/closing/sale/SalesReportTable.tsx | 172 +++++++++++++----- 1 file changed, 124 insertions(+), 48 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 1330b7f0..f0810f15 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -16,6 +16,10 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } +interface FooterSalesRow extends BaseSales { + _isFooter: true; +} + const SalesReportTable = ({ type = 'detail', initialValues, @@ -68,6 +72,29 @@ const SalesReportTable = ({ }; }, [salesData]); + const footerData = useMemo((): FooterSalesRow[] => { + if (salesData.length === 0) return []; + + const footerRow: FooterSalesRow = { + id: -999, + realization_date: 'Total Penjualan', + age: 0, + do_number: '', + product: {} as Product, + customer: {} as Customer, + qty: totals.totalQuantity, + weight: totals.totalWeight, + avg_weight: totals.avgWeight, + price: totals.avgPricePartner, + total_price: totals.totalPartner, + kandang: {} as Kandang, + payment_status: '', + _isFooter: true, + }; + + return [footerRow]; + }, [salesData, totals]); + const salesColumns: ColumnDef[] = useMemo( () => [ { @@ -75,6 +102,14 @@ const SalesReportTable = ({ accessorKey: 'realization_date', header: 'Tanggal Realisasi', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) { + return ( +
+ {props.row.original.realization_date} +
+ ); + } const date = props.row.original.realization_date; return date ? formatDate(date, 'DD MMM YYYY') : '-'; }, @@ -83,19 +118,27 @@ const SalesReportTable = ({ id: 'age', accessorKey: 'age', header: 'Umur', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + return isFooter ? null : props.getValue() || '-'; + }, }, { id: 'do_number', accessorKey: 'do_number', header: 'No. DO', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + return isFooter ? null : props.getValue() || '-'; + }, }, { id: 'product', accessorKey: 'product', header: 'Produk', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; const product = props.getValue() as Product; return product?.name || '-'; }, @@ -105,6 +148,8 @@ const SalesReportTable = ({ accessorKey: 'customer', header: 'Customer', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; const customer = props.getValue() as Customer; return customer?.name || '-'; }, @@ -115,9 +160,13 @@ const SalesReportTable = ({ header: 'Kuantitas', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; + const isFooter = '_isFooter' in props.row.original; return ( -
+
{formatNumber(value)}
); @@ -129,9 +178,13 @@ const SalesReportTable = ({ header: 'Kg', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; + const isFooter = '_isFooter' in props.row.original; return ( -
+
{formatNumber(value)}
); @@ -143,9 +196,13 @@ const SalesReportTable = ({ header: 'AVG (Kg)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; + const isFooter = '_isFooter' in props.row.original; return ( -
+
{formatNumber(value)}
); @@ -157,7 +214,18 @@ const SalesReportTable = ({ header: 'Harga Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -166,7 +234,18 @@ const SalesReportTable = ({ header: 'Total Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -175,7 +254,18 @@ const SalesReportTable = ({ header: 'Harga Act (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -184,7 +274,18 @@ const SalesReportTable = ({ header: 'Total Act (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -192,6 +293,8 @@ const SalesReportTable = ({ accessorKey: 'kandang', header: 'Kandang', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; const kandang = props.getValue() as Kandang; return kandang?.name || '-'; }, @@ -201,6 +304,9 @@ const SalesReportTable = ({ accessorKey: 'payment_status', header: 'Status Pembayaran', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const status = props.getValue() as string; const getStatusColor = (status: string) => { if (!status) return 'neutral'; @@ -239,43 +345,8 @@ const SalesReportTable = ({ 0} - footerContent={ - - - - - - - - - - - - - - - - - - - } className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', @@ -286,6 +357,11 @@ const SalesReportTable = ({ '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', }} />
- Total Penjualan - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} -