mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
Merge branch 'feat/marketing-table-order' into 'development'
[FEAT/FE] Marketing Table Order See merge request mbugroup/lti-web-client!465
This commit is contained in:
@@ -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<SortingState>([]);`
|
||||
|
||||
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 `<Table>`:**
|
||||
|
||||
```tsx
|
||||
<Table sorting={sorting} setSorting={setSorting} ... />
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
@@ -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<SortingState>) => {
|
||||
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<HTMLInputElement> =
|
||||
useCallback((e) => {
|
||||
setExportProgressStartDate(e.target.value);
|
||||
}, []);
|
||||
const exportProgressStartDateChangeHandler: ChangeEventHandler<
|
||||
HTMLInputElement
|
||||
> = (e) => {
|
||||
setExportProgressStartDate(e.target.value);
|
||||
};
|
||||
|
||||
const exportProgressEndDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
|
||||
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<ColumnDef<Marketing>[]>(() => {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user