From 3a2e74b55974522dcdea7f8887886fb9f38bbcbe Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 13 May 2026 10:51:35 +0700 Subject: [PATCH 01/24] feat: implement server-side sorting --- .../pages/expense/ExpensesTable.tsx | 69 +++++++++++------ .../pages/purchase/PurchaseTable.tsx | 74 +++++++++++++++---- 2 files changed, 103 insertions(+), 40 deletions(-) diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 9df40afc..6ce84900 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -1,18 +1,13 @@ 'use client'; -import { - ChangeEventHandler, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, Row, SortingState, + Updater, } from '@tanstack/react-table'; import toast from 'react-hot-toast'; @@ -47,7 +42,8 @@ import { BaseApiResponse } from '@/types/api/api-general'; type ExpenseTableFilters = { search: string; - nameSort: string; + sort_by: string; + order_by: string; transactionDate: string; realizationDate: string; locationId: string; @@ -242,7 +238,8 @@ const ExpensesTable = () => { page: 1, pageSize: 10, search: '', - nameSort: '', + sort_by: '', + order_by: '', transactionDate: '', realizationDate: '', locationId: '', @@ -261,7 +258,8 @@ const ExpensesTable = () => { paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', + sort_by: 'sort_by', + order_by: 'sort_order', transactionDate: 'transaction_date', realizationDate: 'realization_date', locationId: 'location_id', @@ -319,7 +317,26 @@ const ExpensesTable = () => { const [exportProgressStartDate, setExportProgressStartDate] = useState(''); const [exportProgressEndDate, setExportProgressEndDate] = useState(''); - const [sorting, setSorting] = useState([]); + const sorting: SortingState = tableFilterState.sort_by + ? [ + { + id: tableFilterState.sort_by, + desc: tableFilterState.order_by === 'desc', + }, + ] + : []; + + const handleSortingChange = (updater: Updater) => { + const next = typeof updater === 'function' ? updater(sorting) : updater; + if (next.length > 0) { + updateFilter('sort_by', next[0].id, true); + updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true); + } else { + updateFilter('sort_by', '', true); + updateFilter('order_by', '', true); + } + }; + const [rowSelection, setRowSelection] = useState>({}); const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item) @@ -437,10 +454,12 @@ const ExpensesTable = () => { cell: (props) => props.row.original.location?.name ?? '-', }, { + accessorKey: 'created_user', accessorFn: (row) => row.created_user.name ?? '-', header: 'Nama Pengaju', }, { + accessorKey: 'supplier', accessorFn: (row) => row.supplier.name ?? '-', header: 'Uraian', }, @@ -454,17 +473,20 @@ const ExpensesTable = () => { }, { header: 'Status Pencairan', + enableSorting: false, cell: (props) => ( ), }, { header: 'Status BOP', + enableSorting: false, cell: (props) => ( ), }, { + accessorKey: 'is_paid', header: 'Status Lunas', cell: (props) => { return ( @@ -478,6 +500,14 @@ const ExpensesTable = () => { ); }, }, + { + accessorKey: 'created_at', + header: 'Tanggal Dibuat', + cell: (props) => + props.row.original.created_at + ? formatDate(props.row.original.created_at, 'DD MMM YYYY') + : '-', + }, { header: 'Aksi', cell: (props) => { @@ -882,17 +912,6 @@ const ExpensesTable = () => { } }, [getTableFilterQueryString]); - // track sorting - useEffect(() => { - const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); - - if (!isNameSorted) { - updateFilter('nameSort', '', false); - } else { - updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); - } - }, [sorting, updateFilter]); - return ( <>
@@ -1051,7 +1070,8 @@ const ExpensesTable = () => { 'page', 'pageSize', 'search', - 'nameSort', + 'sort_by', + 'order_by', 'userId', 'locationName', 'vendorName', @@ -1152,7 +1172,8 @@ const ExpensesTable = () => { onPageSizeChange={setPageSize} isLoading={isLoading} sorting={sorting} - setSorting={setSorting} + setSorting={handleSortingChange} + manualSorting rowSelection={rowSelection} setRowSelection={setRowSelection} enableRowSelection={tableEnableRowSelectionHandler} diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 781fb2cf..b5d1a1f6 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -2,7 +2,12 @@ import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import useSWR from 'swr'; -import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import { + CellContext, + ColumnDef, + SortingState, + Updater, +} from '@tanstack/react-table'; import toast from 'react-hot-toast'; import Link from 'next/link'; @@ -34,6 +39,8 @@ import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line'; type PurchaseTableFilters = { search: string; + sort_by: string; + order_by: string; po_date: string; approval_status: string; product_category_id: string; @@ -157,18 +164,6 @@ const RowOptionsMenu = ({ }; const PurchaseTable = () => { - // ===== STATE MANAGEMENT ===== - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = - useState(false); - const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); - const [selectedPurchase, setSelectedPurchase] = useState( - null - ); - const [exportProgressStartDate, setExportProgressStartDate] = useState(''); - const [exportProgressEndDate, setExportProgressEndDate] = useState(''); - const [sorting, setSorting] = useState([]); - // ===== TABLE FILTER STATE ===== const { state: tableFilterState, @@ -180,6 +175,8 @@ const PurchaseTable = () => { } = useTableFilter({ initial: { search: '', + sort_by: '', + order_by: '', po_date: '', approval_status: '', product_category_id: '', @@ -198,6 +195,8 @@ const PurchaseTable = () => { paramMap: { page: 'page', pageSize: 'limit', + sort_by: 'sort_by', + order_by: 'sort_order', po_date: 'po_date', approval_status: 'approval_status', product_category_id: 'product_category_id', @@ -219,6 +218,36 @@ const PurchaseTable = () => { storeName: 'purchase-table', }); + // ===== STATE MANAGEMENT ===== + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); + const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); + const [selectedPurchase, setSelectedPurchase] = useState( + null + ); + const [exportProgressStartDate, setExportProgressStartDate] = useState(''); + const [exportProgressEndDate, setExportProgressEndDate] = useState(''); + const sorting: SortingState = tableFilterState.sort_by + ? [ + { + id: tableFilterState.sort_by, + desc: tableFilterState.order_by === 'desc', + }, + ] + : []; + + const handleSortingChange = (updater: Updater) => { + const next = typeof updater === 'function' ? updater(sorting) : updater; + if (next.length > 0) { + updateFilter('sort_by', next[0].id, true); + updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true); + } else { + updateFilter('sort_by', '', true); + updateFilter('order_by', '', true); + } + }; + // ===== MODAL HOOKS ===== const filterModal = useModal(); const deleteModal = useModal(); @@ -238,6 +267,7 @@ const PurchaseTable = () => { const purchaseColumns: ColumnDef[] = [ { header: 'No. PR/PO', + enableSorting: false, cell: (props) => { const { pr_number, po_number } = props.row.original; return po_number ? po_number : pr_number; @@ -278,7 +308,7 @@ const PurchaseTable = () => { cell: (props) => props.row.original.requester_name || '-', }, { - accessorKey: 'products.name', + accessorKey: 'products', header: 'Produk', cell: (props) => { const products = props.row.original.products; @@ -293,7 +323,7 @@ const PurchaseTable = () => { }, }, { - accessorKey: 'location.name', + accessorKey: 'location', header: 'Lokasi', cell: (props) => props.row.original.location?.name || '-', }, @@ -323,6 +353,7 @@ const PurchaseTable = () => { }, { header: 'Aging', + enableSorting: false, cell: (props) => { const purchase = props.row.original; if (!purchase.po_date) return '-'; @@ -334,6 +365,7 @@ const PurchaseTable = () => { }, }, { + accessorKey: 'status', header: 'Status Approval', cell: (props) => { const approval = props.row.original.latest_approval; @@ -378,6 +410,14 @@ const PurchaseTable = () => { ); }, }, + { + accessorKey: 'created_at', + header: 'Tanggal Dibuat', + cell: (props) => + props.row.original.created_at + ? formatDate(props.row.original.created_at, 'DD MMM YYYY') + : '-', + }, { header: 'Aksi', cell: (props) => { @@ -658,6 +698,7 @@ const PurchaseTable = () => { 'search', 'filter_by', 'sort_by', + 'order_by', 'product_category_name', 'supplier_name', 'area_name', @@ -771,7 +812,8 @@ const PurchaseTable = () => { onPageSizeChange={setPageSize} isLoading={isLoading} sorting={sorting} - setSorting={setSorting} + setSorting={handleSortingChange} + manualSorting className={{ containerClassName: cn('p-3 mb-0'), headerColumnClassName: 'text-nowrap', From 280d790f0c20e24f75dd2ab08121270ad05f1524 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 13 May 2026 10:51:46 +0700 Subject: [PATCH 02/24] fix: add created_at column --- src/components/pages/marketing/MarketingTable.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 1026a11a..08dcf318 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -772,6 +772,14 @@ const MarketingTable = () => { } }, }, + { + accessorKey: 'created_at', + header: 'Tanggal Dibuat', + cell: (props) => + props.row.original.created_at + ? formatDate(props.row.original.created_at, 'DD MMM yyyy') + : '-', + }, { id: 'actions', maxSize: 80, From ddffdd1b276cfc2a4bfc0017237e94a3a58f2051 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 13 May 2026 13:46:59 +0700 Subject: [PATCH 03/24] fix: adjust marketing_type default value --- .../marketing/form/MarketingForm.schema.ts | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index aa304315..7e3d21c7 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -123,8 +123,17 @@ export const SalesProductToFieldValues = ( total_price: product.total_price, marketing_type: product.marketing_type ? { - value: product.marketing_type, - label: formatTitleCase(product.marketing_type), + value: + product.marketing_type === 'AYAM' || + product.marketing_type === 'AYAM_PULLET' + ? 'AYAM,AYAM_PULLET' + : product.marketing_type, + label: formatTitleCase( + product.marketing_type === 'AYAM' || + product.marketing_type === 'AYAM_PULLET' + ? 'AYAM' + : product.marketing_type + ), } : null, convertion_unit: product.convertion_unit @@ -185,8 +194,17 @@ export const DeliveryProductToFieldValues = ( marketing_product_id: item.marketing_product_id ?? salesOrder?.id, marketing_type: salesOrder?.marketing_type ? { - value: salesOrder?.marketing_type, - label: formatTitleCase(salesOrder?.marketing_type), + value: + salesOrder?.marketing_type === 'AYAM' || + salesOrder?.marketing_type === 'AYAM_PULLET' + ? 'AYAM,AYAM_PULLET' + : salesOrder?.marketing_type, + label: formatTitleCase( + salesOrder?.marketing_type === 'AYAM' || + salesOrder?.marketing_type === 'AYAM_PULLET' + ? 'AYAM' + : salesOrder?.marketing_type + ), } : null, convertion_unit: salesOrder?.convertion_unit From 58ddd9b99109219a66974ad54baf85571922226c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 13 May 2026 15:26:10 +0700 Subject: [PATCH 04/24] fix: set sortDescFirst false --- src/components/Table.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index d9d81543..2795cbb4 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -173,6 +173,7 @@ const Table = ({ const tableOptions: TableOptions = { columns, data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion + defaultColumn: { sortDescFirst: false }, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), From 910a36857e0647041b7e3e8d6ed91bb7af73a8e5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 13 May 2026 15:26:30 +0700 Subject: [PATCH 05/24] fix: pass the rest of secondaryButton props --- src/components/modal/ConfirmationModalWithNotes.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/modal/ConfirmationModalWithNotes.tsx b/src/components/modal/ConfirmationModalWithNotes.tsx index 20f63019..cf55a7c9 100644 --- a/src/components/modal/ConfirmationModalWithNotes.tsx +++ b/src/components/modal/ConfirmationModalWithNotes.tsx @@ -69,6 +69,7 @@ const ConfirmationModalWithNotes: React.FC = ({ secondaryButton={ secondaryButton ? { + ...secondaryButton, text: secondaryButton?.text ?? 'Tidak', onClick: (e) => { if (secondaryButton && secondaryButton?.onClick) { From fe2a2dfb43fb6c7893366ff6defbcb9d42ca6807 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 13 May 2026 15:29:03 +0700 Subject: [PATCH 06/24] fix: add loading state to approve modal --- .../pages/marketing/MarketingTable.tsx | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 08dcf318..39384b0d 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -297,6 +297,8 @@ const MarketingTable = () => { const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); + const [isApproveLoading, setIsApproveLoading] = useState(false); + const [isDeliveryLoading, setIsDeliveryLoading] = useState(false); const filterResetHandler = () => { updateFilter('product_ids', '', true); @@ -452,23 +454,33 @@ const MarketingTable = () => { return; } - const approveMarketingRes: BaseApiResponse | undefined = - approveAction === 'APPROVED' - ? await MarketingApi.bulkApprovals( - idsToProcess, - nextApprovalStatus as 'SALES_ORDER' | 'DELIVERY_ORDER', - '', - notes || `APPROVED marketing ${idsToProcess.join(', ')}` - ) - : await SalesOrderApi.bulkApprovals(idsToProcess, approveAction, notes); + setIsApproveLoading(true); - if (isResponseSuccess(approveMarketingRes)) { - confirmationModal.closeModal(); - toast.success(approveMarketingRes?.message as string); - setRowSelection({}); + try { + const approveMarketingRes: BaseApiResponse | undefined = + approveAction === 'APPROVED' + ? await MarketingApi.bulkApprovals( + idsToProcess, + nextApprovalStatus as 'SALES_ORDER' | 'DELIVERY_ORDER', + '', + notes || `APPROVED marketing ${idsToProcess.join(', ')}` + ) + : await SalesOrderApi.bulkApprovals( + idsToProcess, + approveAction, + notes + ); + + if (isResponseSuccess(approveMarketingRes)) { + confirmationModal.closeModal(); + toast.success(approveMarketingRes?.message as string); + setRowSelection({}); + } + + refreshMarketing(); + } finally { + setIsApproveLoading(false); } - - refreshMarketing(); }; const bulkDeliveryDateChangeHandler: ChangeEventHandler = ( @@ -530,13 +542,21 @@ const MarketingTable = () => { }; const confirmationModalDeliveryClickHandler = async (notes: string) => { - const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes); - deliveryModal.closeModal(); - toast.success(res?.message as string); - refreshMarketing?.(); - router.push( - `/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}` - ); + setIsDeliveryLoading(true); + try { + const res = await SalesOrderApi.delivery( + selectedItem?.id as number, + notes + ); + deliveryModal.closeModal(); + toast.success(res?.message as string); + refreshMarketing?.(); + router.push( + `/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}` + ); + } finally { + setIsDeliveryLoading(false); + } }; const getRowCanSelect = useCallback( @@ -1020,11 +1040,13 @@ const MarketingTable = () => { text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan tahap ${selectedApprovalStep ?? '-'} (${idsToProcess.length} data)?`} secondaryButton={{ text: 'Tidak', + isLoading: isApproveLoading, onClick: confirmationModal.closeModal, }} primaryButton={{ text: 'Ya', color: approveAction === 'APPROVED' ? 'success' : 'error', + isLoading: isApproveLoading, onClick: approveMarketingHandler, }} /> @@ -1048,10 +1070,12 @@ const MarketingTable = () => { text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`} secondaryButton={{ text: 'Tidak', + isLoading: isDeliveryLoading, }} primaryButton={{ text: 'Ya', color: 'success', + isLoading: isDeliveryLoading, onClick: confirmationModalDeliveryClickHandler, }} /> @@ -1111,6 +1135,7 @@ const MarketingTable = () => {
+ {header.category === 'empty_kandang' && ( +
+ +

+ {header.empty_kandang_end_date + ? formatDate(header.empty_kandang_end_date) + : '-'} +

+
+ )}
{getStatusBadge(header.status)}
From 770c29325765e98977c6a93d445819ffb171de14 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 18 May 2026 10:25:27 +0700 Subject: [PATCH 13/24] fix: adjust empty_kandang type in BaseDailyChecklist --- src/types/api/daily-checklist/daily-checklist.d.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/types/api/daily-checklist/daily-checklist.d.ts b/src/types/api/daily-checklist/daily-checklist.d.ts index 139ff894..4099c52d 100644 --- a/src/types/api/daily-checklist/daily-checklist.d.ts +++ b/src/types/api/daily-checklist/daily-checklist.d.ts @@ -12,8 +12,11 @@ export type BaseDailyChecklist = { status: string; category: string; date: string; - empty_kandang?: boolean; - empty_kandang_end_date?: string | null; + empty_kandang?: { + id: boolean; + start_date: string; + end_date: string; + }; kandang?: Pick; total_phase: number; total_activity: number; From 0b63dcb532c5e7025694ac6e436f51e6c1d6f791 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 18 May 2026 11:37:40 +0700 Subject: [PATCH 14/24] feat: implement server-side sorting in FinanceTable --- src/components/pages/finance/FinanceTable.tsx | 84 +++++++++++++++---- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 998193e7..211af1f7 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -1,7 +1,12 @@ 'use client'; import React, { useEffect, useMemo, useState } from 'react'; -import { CellContext, ColumnDef } from '@tanstack/react-table'; +import { + CellContext, + ColumnDef, + SortingState, + Updater, +} from '@tanstack/react-table'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { useFormik } from 'formik'; @@ -183,7 +188,8 @@ const FinanceTable = () => { bankIds: '', customerIds: '', supplierIds: '', - sortBy: '', + sort_by: '', + orderBy: '', startDate: '', endDate: '', bankNames: '', @@ -197,7 +203,8 @@ const FinanceTable = () => { bankIds: 'bank_ids', customerIds: 'customer_ids', supplierIds: 'supplier_ids', - sortBy: 'sort_date', + sort_by: 'sort_by', + orderBy: 'sort_order', startDate: 'start_date', endDate: 'end_date', }, @@ -248,7 +255,7 @@ const FinanceTable = () => { updateFilter('bankIds', values.bank_ids, true); updateFilter('customerIds', values.customer_ids, true); updateFilter('supplierIds', values.supplier_ids, true); - updateFilter('sortBy', values.sort_by, true); + updateFilter('sort_by', values.sort_by, true); updateFilter('startDate', values.start_date, true); updateFilter('endDate', values.end_date, true); // Save display names for restoration on modal reopen @@ -276,7 +283,8 @@ const FinanceTable = () => { updateFilter('bankIds', '', true); updateFilter('customerIds', '', true); updateFilter('supplierIds', '', true); - updateFilter('sortBy', '', true); + updateFilter('sort_by', '', true); + updateFilter('orderBy', '', true); updateFilter('startDate', '', true); updateFilter('endDate', '', true); updateFilter('bankNames', '', true); @@ -394,6 +402,26 @@ const FinanceTable = () => { ); }; + const sorting: SortingState = tableFilterState.sort_by + ? [ + { + id: tableFilterState.sort_by, + desc: tableFilterState.orderBy === 'desc', + }, + ] + : []; + + const handleSortingChange = (updater: Updater) => { + const next = typeof updater === 'function' ? updater(sorting) : updater; + if (next.length > 0) { + updateFilter('sort_by', next[0].id, true); + updateFilter('orderBy', next[0].desc ? 'desc' : 'asc', true); + } else { + updateFilter('sort_by', '', true); + updateFilter('orderBy', '', true); + } + }; + const startDateChangeHandler = (e: React.ChangeEvent) => { const value = e.target.value; const endDate = filterFormik.values.end_date; @@ -505,7 +533,7 @@ const FinanceTable = () => { // Restore sort by const restoredSortBy = sortByOptions.find( - (opt) => String(opt.value) === tableFilterState.sortBy + (opt) => String(opt.value) === tableFilterState.sort_by ) || null; setSelectedSortBy(restoredSortBy); @@ -516,7 +544,7 @@ const FinanceTable = () => { bank_ids: tableFilterState.bankIds || '', customer_ids: tableFilterState.customerIds || '', supplier_ids: tableFilterState.supplierIds || '', - sort_by: tableFilterState.sortBy || '', + sort_by: tableFilterState.sort_by || '', start_date: tableFilterState.startDate || '', end_date: tableFilterState.endDate || '', }); @@ -540,10 +568,12 @@ const FinanceTable = () => { { header: 'ID', accessorKey: 'payment_code', + enableSorting: true, }, { header: 'References Number', accessorKey: 'reference_number', + enableSorting: true, cell: (props: CellContext) => { const value = props.row.original.reference_number; return {value ?? '-'}; @@ -552,6 +582,7 @@ const FinanceTable = () => { { header: 'Jenis Transaksi', accessorKey: 'transaction_type', + enableSorting: true, cell: (props: CellContext) => { const value = props.row.original.transaction_type .split('_') @@ -561,7 +592,8 @@ const FinanceTable = () => { }, { header: 'Pihak', - accessorFn: (finance: Finance) => finance.party?.name, + accessorKey: 'customer_name', + enableSorting: true, cell: (props: CellContext) => { if (props.row.original.party?.id) { return {props.row.original.party?.name}; @@ -571,16 +603,22 @@ const FinanceTable = () => { }, { header: 'Tanggal Pembayaran', - accessorFn: (finance: Finance) => - formatDate(finance.payment_date, 'DD MMM YYYY'), + accessorKey: 'payment_date', + enableSorting: true, + cell: (props) => + formatDate(props.row.original.payment_date, 'DD MMM YYYY'), }, { header: 'Tanggal Dibuat', - accessorFn: (finance) => formatDate(finance.created_at, 'DD MMM YYYY'), + accessorKey: 'created_at', + enableSorting: true, + cell: (props) => + formatDate(props.row.original.created_at, 'DD MMM YYYY'), }, { header: 'Metode Pembayaran', accessorKey: 'payment_method', + enableSorting: true, cell: (props: CellContext) => { const value = props.row.original.payment_method.split('_').join(' '); return {formatTitleCase(value)}; @@ -588,20 +626,26 @@ const FinanceTable = () => { }, { header: 'Bank', - accessorFn: (finance: Finance) => - finance.bank - ? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}` + accessorKey: 'bank', + enableSorting: true, + cell: (props) => + props.row.original.bank + ? `${props.row.original.bank?.alias} - ${props.row.original.bank?.account_number} - ${props.row.original.bank?.owner}` : '-', }, { header: 'Pengeluaran (Rp)', - accessorFn: (finance: Finance) => - formatCurrency(Math.abs(finance.expense_amount)), + accessorKey: 'expense_amount', + enableSorting: true, + cell: (props) => + formatCurrency(Math.abs(props.row.original.expense_amount)), }, { header: 'Pemasukan (Rp)', - accessorFn: (finance: Finance) => - formatCurrency(Math.abs(finance.income_amount)), + accessorKey: 'income_amount', + enableSorting: true, + cell: (props) => + formatCurrency(Math.abs(props.row.original.income_amount)), }, { header: 'Aksi', @@ -707,6 +751,7 @@ const FinanceTable = () => { 'page', 'pageSize', 'search', + 'orderBy', 'bankNames', 'customerNames', 'supplierNames', @@ -749,6 +794,9 @@ const FinanceTable = () => { onPageChange={setPage} onPageSizeChange={setPageSize} isLoading={isLoading} + sorting={sorting} + setSorting={handleSortingChange} + manualSorting className={{ containerClassName: cn('p-3 mb-0'), headerColumnClassName: 'text-nowrap', From 001dafecb72c8abdb814b72a5ac8451744d23b3a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 18 May 2026 14:18:35 +0700 Subject: [PATCH 15/24] fix: adjust copywriting for approve button based on approval step number --- src/components/pages/marketing/DeliveryOrderFormModal.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index d6ace126..fe98603d 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -849,7 +849,11 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold' disabled={deliveryRejected} > - Approve + {marketing?.data?.latest_approval?.step_number === 1 && + 'Approve'} + + {marketing?.data?.latest_approval?.step_number === 2 && + 'Deliver Item'}
)} From 6c6f739fc05d4180e63b0f5993f4007cdc5579dc Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 18 May 2026 14:20:30 +0700 Subject: [PATCH 16/24] fix: remove onAfterSubmit callback in useFormikErrorList --- src/components/pages/marketing/SalesOrderFormModal.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/pages/marketing/SalesOrderFormModal.tsx b/src/components/pages/marketing/SalesOrderFormModal.tsx index 17a59a92..77f9c92b 100644 --- a/src/components/pages/marketing/SalesOrderFormModal.tsx +++ b/src/components/pages/marketing/SalesOrderFormModal.tsx @@ -246,6 +246,7 @@ const SalesOrderFormModal = ({ }) .filter((item) => Boolean(item)), } as UpdateDeliveryOrderPayload); + switch (modalAction) { case 'add': await createMarketingHandler(payload as CreateSalesOrderPayload); @@ -261,11 +262,7 @@ const SalesOrderFormModal = ({ // ===== Formik Error List ===== const { formErrorList, setFormErrorList, close, handleFormSubmit } = - useFormikErrorList(formik, { - onAfterSubmit: () => { - router.push('/marketing'); - }, - }); + useFormikErrorList(formik); // ================== FORM REPEATER HANDLER ================== const createMarketingHandler = async (values: CreateSalesOrderPayload) => { From 82b5429d021485a483a1325d10745f79a13bfe18 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 18 May 2026 14:24:59 +0700 Subject: [PATCH 17/24] fix: update DeliveryOrderSchema validation, make all delivery_order should valid instead of some --- src/components/pages/marketing/form/MarketingForm.schema.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index 7e3d21c7..385f1c90 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -71,14 +71,14 @@ export const DeliveryOrderSchema: Yup.ObjectSchema = .required('Pengiriman wajib diisi!') .test( 'at-least-one-valid-row', - 'Minimal harus ada satu baris pengiriman yang lengkap diisi!', + 'Seluruh data pengiriman harus diisi lengkap!', function (items) { if (!items || items.length === 0) return false; - // VALIDASI: minimal 1 item valid full + // VALIDASI: seluruh item harus valid full const itemSchema = DeliveryOrderProductSchema; - const hasValidItem = items.some((item) => { + const hasValidItem = items.every((item) => { if (!item) return false; return itemSchema.isValidSync(item, { abortEarly: true }); }); From 910981645b253a9ca952484d539e1f168209340f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 18 May 2026 14:25:19 +0700 Subject: [PATCH 18/24] fix: remove unnecessary code --- .../delivery-order/DeliverOrderProduct.tsx | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index 71716a70..f42ac710 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -146,15 +146,6 @@ const DeliveryOrderProductForm = ({ ); // ============ Fetch Data ============ - const { data: productData } = useSWR( - selectedProduct?.value - ? ProductApi.basePath + '/' + selectedProduct?.value - : null, - () => - selectedProduct?.value - ? ProductApi.getSingle(Number(selectedProduct?.value)) - : undefined - ); // Options Week dari minggu 1 - 22 // const optionsWeek = useMemo(() => { @@ -440,7 +431,8 @@ const DeliveryOrderProductForm = ({ handleBlurField(currentInput); formik.setFieldValue( 'uom', - isResponseSuccess(productData) ? productData?.data?.uom?.name : '' + initialValues?.marketing_product?.product_warehouse_data?.product?.uom + ?.name ?? '' ); }, } @@ -813,9 +805,8 @@ const DeliveryOrderProductForm = ({ endAdornment={
- {isResponseSuccess(productData) - ? productData?.data?.uom.name - : ''} + {initialValues?.marketing_product?.product_warehouse_data + ?.product?.uom?.name ?? ''}
} @@ -826,9 +817,8 @@ const DeliveryOrderProductForm = ({ (item) => item.id === formik.values.marketing_product_id )?.qty + ' ' + - (isResponseSuccess(productData) - ? productData?.data?.uom.name - : '') + (initialValues?.marketing_product?.product_warehouse_data + ?.product?.uom?.name ?? '') : '' } /> From c12beca4d709633f8c0ad72660be2941d779f7c1 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 18 May 2026 14:26:52 +0700 Subject: [PATCH 19/24] fix: recalculate qty if product change --- .../form/repeater/sales-order/SalesOrderProductForm.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index 5f0031f8..bcc71131 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -252,6 +252,11 @@ const SalesOrderProductForm = ({ setSelectedProductWarehouse(productWarehouse || null); formik.setFieldValue('product_warehouse_data', productWarehouse || null); formik.setFieldValue('qty', productWarehouse?.quantity); + + if (productWarehouse?.quantity) { + handleFieldChange('qty', productWarehouse?.quantity); + } + formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || ''); if ( productWarehouse?.week !== undefined && From fd7b49ab93c7168c585009cbe359307d21f8898a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 19 May 2026 11:51:17 +0700 Subject: [PATCH 20/24] feat: implement server-side sorting in report expense --- .../report/expense/tab/ReportExpenseTab.tsx | 64 ++++++++++++++----- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index edd5b725..8e6b49b5 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -39,7 +39,7 @@ import { } from '@/services/api/master-data'; import { Supplier } from '@/types/api/master-data/supplier'; import { Nonstock } from '@/types/api/master-data/nonstock'; -import { ColumnDef } from '@tanstack/react-table'; +import { ColumnDef, SortingState, Updater } from '@tanstack/react-table'; import { httpClient } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import ButtonFilter from '@/components/helper/ButtonFilter'; @@ -73,6 +73,25 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); + // ===== SORTING STATE ===== + const [sortBy, setSortBy] = useState(''); + const [orderBy, setOrderBy] = useState(''); + + const sorting: SortingState = sortBy + ? [{ id: sortBy, desc: orderBy === 'desc' }] + : []; + + const handleSortingChange = (updater: Updater) => { + const next = typeof updater === 'function' ? updater(sorting) : updater; + if (next.length > 0) { + setSortBy(next[0].id); + setOrderBy(next[0].desc ? 'desc' : 'asc'); + } else { + setSortBy(''); + setOrderBy(''); + } + }; + const handleFilterModalOpenRef = useRef(() => {}); const filterModal = useModal(); @@ -252,6 +271,8 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { if (filterParams.category) { params.append('category', filterParams.category); } + if (sortBy) params.append('sort_by', sortBy); + if (orderBy) params.append('sort_order', orderBy); Object.entries(extraParams ?? {}).forEach(([key, value]) => { params.set(key, value); @@ -259,7 +280,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { return params.toString(); }, - [filterParams] + [filterParams, sortBy, orderBy] ); // ===== DATA FETCHING ===== @@ -443,19 +464,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { return [ { header: 'No', + enableSorting: false, cell: (props) => (page - 1) * pageSize + props.row.index + 1, }, { header: 'No. PO', accessorKey: 'po_number', + enableSorting: true, }, { header: 'No. Referensi', accessorKey: 'reference_number', + enableSorting: true, }, { header: 'Tanggal Realisasi', accessorKey: 'realization_date', + enableSorting: true, cell: ({ row }) => { return formatDate(row.original?.realization_date, 'DD MMM, YYYY'); }, @@ -463,6 +488,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { { header: 'Tanggal Transaksi', accessorKey: 'transaction_date', + enableSorting: true, cell: ({ row }) => { return formatDate(row.original?.transaction_date, 'DD MMM, YYYY'); }, @@ -470,21 +496,30 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { { header: 'Kategori', accessorKey: 'category', + enableSorting: true, }, { header: 'Produk', + accessorKey: 'product', + enableSorting: true, accessorFn: (row) => row.pengajuan?.nonstock?.name, }, { header: 'Supplier', + accessorKey: 'supplier', + enableSorting: true, accessorFn: (row) => row.supplier?.name, }, { header: 'Lokasi', + accessorKey: 'location', + enableSorting: true, accessorFn: (row) => row.kandang?.location?.name, }, { header: 'Kandang', + accessorKey: 'kandang', + enableSorting: true, accessorFn: (row) => row.kandang?.name, }, { @@ -492,23 +527,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { columns: [ { header: 'Qty', - id: 'qty_pengajuan', - accessorFn: (row) => row.pengajuan?.qty, + accessorKey: 'qty_pengajuan', cell: ({ row }) => row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0', }, { header: 'Harga', - id: 'harga_pengajuan', - accessorFn: (row) => row.pengajuan?.price, + accessorKey: 'price_pengajuan', cell: ({ row }) => formatCurrency(row.original.pengajuan?.price || 0), }, { header: 'Total', - id: 'total_pengajuan', - accessorFn: (row) => - (row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0), + accessorKey: 'total_pengajuan', cell: ({ row }) => { const total = (row.original.pengajuan?.qty || 0) * @@ -523,23 +554,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { columns: [ { header: 'Qty', - id: 'qty_realisasi', - accessorFn: (row) => row.realisasi?.qty, + accessorKey: 'qty_realisasi', cell: ({ row }) => row.original.realisasi?.qty?.toLocaleString('id-ID') || '0', }, { header: 'Harga', - id: 'harga_realisasi', - accessorFn: (row) => row.realisasi?.price, + accessorKey: 'price_realisasi', cell: ({ row }) => formatCurrency(row.original.realisasi?.price || 0), }, { header: 'Total', - id: 'total_realisasi', - accessorFn: (row) => - (row.realisasi?.qty || 0) * (row.realisasi?.price || 0), + accessorKey: 'total_realisasi', cell: ({ row }) => { const total = (row.original.realisasi?.qty || 0) * @@ -550,6 +577,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { ], }, { + id: 'realization_status', header: 'Status Pencairan', cell: (props) => ( { ), }, { + id: 'bop_status', header: 'Status BOP', cell: (props) => ( @@ -602,6 +631,9 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { totalItems={meta?.total_results || 0} onPageChange={setPage} onPageSizeChange={setPageSize} + sorting={sorting} + setSorting={handleSortingChange} + manualSorting className={{ containerClassName: 'w-full mb-0!', tableWrapperClassName: 'overflow-x-auto', From 802bf77bc508fa16dd78d54b44fe4a58ded1a25a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 19 May 2026 11:51:27 +0700 Subject: [PATCH 21/24] feat: add rtk instructions --- CLAUDE.md | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index d8f15df6..711d5a1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -260,3 +260,155 @@ const handleExportExcel = useCallback(async () => { - Do **not** import `xlsx`, `@react-pdf/renderer`, `jspdf`, `exceljs` in page/tab components. **Reference implementation:** `MarketingReportApiService.exportDailyMarketingToExcel` / `exportDailyMarketingToPDF` in [src/services/api/report/marketing-report.ts](src/services/api/report/marketing-report.ts), consumed by [src/components/pages/report/marketing/tab/DailyMarketingTab.tsx](src/components/pages/report/marketing/tab/DailyMarketingTab.tsx). + + + +# RTK (Rust Token Killer) - Token-Optimized Commands + +## Golden Rule + +**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use. + +**Important**: Even in command chains with `&&`, use `rtk`: + +```bash +# ❌ Wrong +git add . && git commit -m "msg" && git push + +# ✅ Correct +rtk git add . && rtk git commit -m "msg" && rtk git push +``` + +## RTK Commands by Workflow + +### Build & Compile (80-90% savings) + +```bash +rtk cargo build # Cargo build output +rtk cargo check # Cargo check output +rtk cargo clippy # Clippy warnings grouped by file (80%) +rtk tsc # TypeScript errors grouped by file/code (83%) +rtk lint # ESLint/Biome violations grouped (84%) +rtk prettier --check # Files needing format only (70%) +rtk next build # Next.js build with route metrics (87%) +``` + +### Test (60-99% savings) + +```bash +rtk cargo test # Cargo test failures only (90%) +rtk go test # Go test failures only (90%) +rtk jest # Jest failures only (99.5%) +rtk vitest # Vitest failures only (99.5%) +rtk playwright test # Playwright failures only (94%) +rtk pytest # Python test failures only (90%) +rtk rake test # Ruby test failures only (90%) +rtk rspec # RSpec test failures only (60%) +rtk test # Generic test wrapper - failures only +``` + +### Git (59-80% savings) + +```bash +rtk git status # Compact status +rtk git log # Compact log (works with all git flags) +rtk git diff # Compact diff (80%) +rtk git show # Compact show (80%) +rtk git add # Ultra-compact confirmations (59%) +rtk git commit # Ultra-compact confirmations (59%) +rtk git push # Ultra-compact confirmations +rtk git pull # Ultra-compact confirmations +rtk git branch # Compact branch list +rtk git fetch # Compact fetch +rtk git stash # Compact stash +rtk git worktree # Compact worktree +``` + +Note: Git passthrough works for ALL subcommands, even those not explicitly listed. + +### GitHub (26-87% savings) + +```bash +rtk gh pr view # Compact PR view (87%) +rtk gh pr checks # Compact PR checks (79%) +rtk gh run list # Compact workflow runs (82%) +rtk gh issue list # Compact issue list (80%) +rtk gh api # Compact API responses (26%) +``` + +### JavaScript/TypeScript Tooling (70-90% savings) + +```bash +rtk pnpm list # Compact dependency tree (70%) +rtk pnpm outdated # Compact outdated packages (80%) +rtk pnpm install # Compact install output (90%) +rtk npm run