diff --git a/CLAUDE.md b/CLAUDE.md index d0a2f23c..06bbd33d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,6 +161,45 @@ const handleFilterLocationChange = useCallback( - `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/` - Use same pattern for data tables in other modules (inventory, finance, purchase, etc.) +## Server-side sorting pattern + +Data tables use TanStack Table's `SortingState` wired to `useTableFilter` so that sorting triggers a server re-fetch rather than client-side reordering. + +**Four-part wiring:** + +1. **Local sort state** — `const [sorting, setSorting] = useState([]);` + +2. **`useTableFilter` config** — Add `sort_by` and `order_by` to `initial` and `paramMap`. The `paramMap` key is the internal name; the value is the query param name sent to the server (they can differ, e.g. `order_by` → `sort_order`): + + ```ts + initial: { sort_by: '', order_by: '' } + paramMap: { sort_by: 'sort_by', order_by: 'sort_order' } + ``` + +3. **`useEffect` sync** — Watches `sorting` and pushes changes into `useTableFilter`: + + ```ts + useEffect(() => { + if (sorting.length > 0) { + updateFilter('sort_by', sorting[0].id, true); + updateFilter('order_by', sorting[0].desc ? 'desc' : 'asc', true); + } else { + updateFilter('sort_by', ''); + updateFilter('order_by', ''); + } + }, [sorting]); + ``` + +4. **SWR key** — SWR uses `getTableFilterToQueryString()` as its key, so any filter change (including sort) automatically re-fetches with the new query params. TanStack Table's built-in client sorting is effectively disabled; the server does the sorting. + +**Pass `sorting` / `setSorting` to ``:** + +```tsx +
+``` + +**Reference implementation:** `MarketingTable` in [src/components/pages/marketing/MarketingTable.tsx](src/components/pages/marketing/MarketingTable.tsx). + ## Server-side file export pattern All file exports (Excel, PDF, or any format) must use **server-side generation** — the server returns a binary blob and the browser triggers a download. Never generate files client-side with `xlsx`, `@react-pdf/renderer`, `jspdf`, or similar libraries. diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 06949171..c901c571 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -1,6 +1,5 @@ 'use client'; -import axios from 'axios'; import Button from '@/components/Button'; import CheckboxInput from '@/components/input/CheckboxInput'; import DateInput from '@/components/input/DateInput'; @@ -27,7 +26,13 @@ import { MarketingFilter, } from '@/types/api/marketing/marketing'; import { Icon } from '@iconify/react'; -import { CellContext, ColumnDef, Row } from '@tanstack/react-table'; +import { + CellContext, + ColumnDef, + Row, + SortingState, + Updater, +} from '@tanstack/react-table'; import { useRouter } from 'next/navigation'; import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; @@ -198,6 +203,8 @@ const MarketingTable = () => { project_flock_name: '', project_flock_kandang_id: '', project_flock_kandang_name: '', + sort_by: '', + order_by: '', }, paramMap: { page: 'page', @@ -207,6 +214,8 @@ const MarketingTable = () => { customer_id: 'customer_id', project_flock_id: 'project_flock_id', project_flock_kandang_id: 'project_flock_kandang_id', + sort_by: 'sort_by', + order_by: 'sort_order', }, excludeKeysFromUrl: [ 'product_names', @@ -220,6 +229,26 @@ const MarketingTable = () => { storeName: 'marketing-table', }); + 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); + } + }; + // ===== FETCH DATA ===== const { data: marketing, @@ -359,55 +388,50 @@ const MarketingTable = () => { ? 'DELIVERY_ORDER' : null; - const marketingFilterInitialValues = useMemo(() => { - const productIds = tableFilterState.product_ids - ? tableFilterState.product_ids - .split(',') - .map((item) => item.trim()) - .filter(Boolean) - : []; + const productIds = tableFilterState.product_ids + ? tableFilterState.product_ids + .split(',') + .map((item) => item.trim()) + .filter(Boolean) + : []; - const productLabels = tableFilterState.product_names - ? tableFilterState.product_names - .split(',') - .map((item) => item.trim()) - .filter(Boolean) - : []; + const productLabels = tableFilterState.product_names + ? tableFilterState.product_names + .split(',') + .map((item) => item.trim()) + .filter(Boolean) + : []; - return { - product_ids: productIds.map((value, idx) => ({ - value: Number(value), - label: productLabels[idx] || '-', - })), - status: tableFilterState.status - ? { - value: tableFilterState.status, - label: tableFilterState.status_name, - } - : null, - - customer: tableFilterState.customer_id - ? { - value: Number(tableFilterState.customer_id), - label: tableFilterState.customer_name, - } - : null, - - project_flock: tableFilterState.project_flock_id - ? { - value: Number(tableFilterState.project_flock_id), - label: tableFilterState.project_flock_name, - } - : null, - - project_flock_kandang: tableFilterState.project_flock_kandang_id - ? { - value: Number(tableFilterState.project_flock_kandang_id), - label: tableFilterState.project_flock_kandang_name, - } - : null, - }; - }, [tableFilterState]); + const marketingFilterInitialValues = { + product_ids: productIds.map((value, idx) => ({ + value: Number(value), + label: productLabels[idx] || '-', + })), + status: tableFilterState.status + ? { + value: tableFilterState.status, + label: tableFilterState.status_name, + } + : null, + customer: tableFilterState.customer_id + ? { + value: Number(tableFilterState.customer_id), + label: tableFilterState.customer_name, + } + : null, + project_flock: tableFilterState.project_flock_id + ? { + value: Number(tableFilterState.project_flock_id), + label: tableFilterState.project_flock_name, + } + : null, + project_flock_kandang: tableFilterState.project_flock_kandang_id + ? { + value: Number(tableFilterState.project_flock_kandang_id), + label: tableFilterState.project_flock_kandang_name, + } + : null, + }; const approveMarketingHandler = async (notes: string) => { if (idsToProcess.length === 0) { @@ -542,27 +566,29 @@ const MarketingTable = () => { setIsLoadingExportingToExcel(false); }; - const resetExportProgressForm = useCallback(() => { + const resetExportProgressForm = () => { setExportProgressStartDate(''); setExportProgressEndDate(''); - }, []); + }; - const exportProgressStartDateChangeHandler: ChangeEventHandler = - useCallback((e) => { - setExportProgressStartDate(e.target.value); - }, []); + const exportProgressStartDateChangeHandler: ChangeEventHandler< + HTMLInputElement + > = (e) => { + setExportProgressStartDate(e.target.value); + }; - const exportProgressEndDateChangeHandler: ChangeEventHandler = - useCallback((e) => { - setExportProgressEndDate(e.target.value); - }, []); + const exportProgressEndDateChangeHandler: ChangeEventHandler< + HTMLInputElement + > = (e) => { + setExportProgressEndDate(e.target.value); + }; - const exportProgressInputToExcelClickHandler = useCallback(() => { + const exportProgressInputToExcelClickHandler = () => { resetExportProgressForm(); exportProgressInputModal.openModal(); - }, [exportProgressInputModal, resetExportProgressForm]); + }; - const submitExportProgressInputHandler = useCallback(async () => { + const submitExportProgressInputHandler = async () => { if (!exportProgressStartDate || !exportProgressEndDate) { return; } @@ -585,12 +611,7 @@ const MarketingTable = () => { } finally { setIsExportProgressLoading(false); } - }, [ - exportProgressEndDate, - exportProgressInputModal, - exportProgressStartDate, - resetExportProgressForm, - ]); + }; const columns = useMemo[]>(() => { return [ @@ -656,7 +677,7 @@ const MarketingTable = () => { }, }, { - accessorKey: 'so_do_number', + accessorKey: 'so_number', header: 'No. Order', cell: (props) => { return props.row.original.do_number @@ -672,7 +693,7 @@ const MarketingTable = () => { }, }, { - accessorKey: 'approval.step_name', + accessorKey: 'status', header: 'Status', cell: (props) => { const approval = props.row.original.latest_approval; @@ -707,10 +728,12 @@ const MarketingTable = () => { }, }, { - accessorKey: 'customer.name', + accessorKey: 'customer', header: 'Customer', + cell: (props) => props.row.original.customer.name, }, { + accessorKey: 'grand_total', accessorFn: (row) => row.sales_order ?.map((product) => product.total_price) @@ -727,6 +750,7 @@ const MarketingTable = () => { { accessorKey: 'marketing_products.length', header: 'Product Details', + enableSorting: false, cell: (props) => { if (props?.row?.original?.sales_order?.length) { if (props?.row?.original?.sales_order?.length > 1) { @@ -949,6 +973,8 @@ const MarketingTable = () => { columns={columns} pageSize={tableFilterState.pageSize} page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1} + sorting={sorting} + setSorting={handleSortingChange} totalItems={ isResponseSuccess(marketing) ? marketing?.meta?.total_results