mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 07:15:44 +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/`
|
- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/`
|
||||||
- Use same pattern for data tables in other modules (inventory, finance, purchase, etc.)
|
- 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
|
## 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.
|
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';
|
'use client';
|
||||||
|
|
||||||
import axios from 'axios';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
@@ -27,7 +26,13 @@ import {
|
|||||||
MarketingFilter,
|
MarketingFilter,
|
||||||
} from '@/types/api/marketing/marketing';
|
} from '@/types/api/marketing/marketing';
|
||||||
import { Icon } from '@iconify/react';
|
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 { useRouter } from 'next/navigation';
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -198,6 +203,8 @@ const MarketingTable = () => {
|
|||||||
project_flock_name: '',
|
project_flock_name: '',
|
||||||
project_flock_kandang_id: '',
|
project_flock_kandang_id: '',
|
||||||
project_flock_kandang_name: '',
|
project_flock_kandang_name: '',
|
||||||
|
sort_by: '',
|
||||||
|
order_by: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -207,6 +214,8 @@ const MarketingTable = () => {
|
|||||||
customer_id: 'customer_id',
|
customer_id: 'customer_id',
|
||||||
project_flock_id: 'project_flock_id',
|
project_flock_id: 'project_flock_id',
|
||||||
project_flock_kandang_id: 'project_flock_kandang_id',
|
project_flock_kandang_id: 'project_flock_kandang_id',
|
||||||
|
sort_by: 'sort_by',
|
||||||
|
order_by: 'sort_order',
|
||||||
},
|
},
|
||||||
excludeKeysFromUrl: [
|
excludeKeysFromUrl: [
|
||||||
'product_names',
|
'product_names',
|
||||||
@@ -220,6 +229,26 @@ const MarketingTable = () => {
|
|||||||
storeName: 'marketing-table',
|
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 =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
data: marketing,
|
data: marketing,
|
||||||
@@ -359,55 +388,50 @@ const MarketingTable = () => {
|
|||||||
? 'DELIVERY_ORDER'
|
? 'DELIVERY_ORDER'
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const marketingFilterInitialValues = useMemo(() => {
|
const productIds = tableFilterState.product_ids
|
||||||
const productIds = tableFilterState.product_ids
|
? tableFilterState.product_ids
|
||||||
? tableFilterState.product_ids
|
.split(',')
|
||||||
.split(',')
|
.map((item) => item.trim())
|
||||||
.map((item) => item.trim())
|
.filter(Boolean)
|
||||||
.filter(Boolean)
|
: [];
|
||||||
: [];
|
|
||||||
|
|
||||||
const productLabels = tableFilterState.product_names
|
const productLabels = tableFilterState.product_names
|
||||||
? tableFilterState.product_names
|
? tableFilterState.product_names
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return {
|
const marketingFilterInitialValues = {
|
||||||
product_ids: productIds.map((value, idx) => ({
|
product_ids: productIds.map((value, idx) => ({
|
||||||
value: Number(value),
|
value: Number(value),
|
||||||
label: productLabels[idx] || '-',
|
label: productLabels[idx] || '-',
|
||||||
})),
|
})),
|
||||||
status: tableFilterState.status
|
status: tableFilterState.status
|
||||||
? {
|
? {
|
||||||
value: tableFilterState.status,
|
value: tableFilterState.status,
|
||||||
label: tableFilterState.status_name,
|
label: tableFilterState.status_name,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
customer: tableFilterState.customer_id
|
||||||
customer: tableFilterState.customer_id
|
? {
|
||||||
? {
|
value: Number(tableFilterState.customer_id),
|
||||||
value: Number(tableFilterState.customer_id),
|
label: tableFilterState.customer_name,
|
||||||
label: tableFilterState.customer_name,
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
project_flock: tableFilterState.project_flock_id
|
||||||
|
? {
|
||||||
project_flock: tableFilterState.project_flock_id
|
value: Number(tableFilterState.project_flock_id),
|
||||||
? {
|
label: tableFilterState.project_flock_name,
|
||||||
value: Number(tableFilterState.project_flock_id),
|
}
|
||||||
label: tableFilterState.project_flock_name,
|
: null,
|
||||||
}
|
project_flock_kandang: tableFilterState.project_flock_kandang_id
|
||||||
: null,
|
? {
|
||||||
|
value: Number(tableFilterState.project_flock_kandang_id),
|
||||||
project_flock_kandang: tableFilterState.project_flock_kandang_id
|
label: tableFilterState.project_flock_kandang_name,
|
||||||
? {
|
}
|
||||||
value: Number(tableFilterState.project_flock_kandang_id),
|
: null,
|
||||||
label: tableFilterState.project_flock_kandang_name,
|
};
|
||||||
}
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
}, [tableFilterState]);
|
|
||||||
|
|
||||||
const approveMarketingHandler = async (notes: string) => {
|
const approveMarketingHandler = async (notes: string) => {
|
||||||
if (idsToProcess.length === 0) {
|
if (idsToProcess.length === 0) {
|
||||||
@@ -542,27 +566,29 @@ const MarketingTable = () => {
|
|||||||
setIsLoadingExportingToExcel(false);
|
setIsLoadingExportingToExcel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetExportProgressForm = useCallback(() => {
|
const resetExportProgressForm = () => {
|
||||||
setExportProgressStartDate('');
|
setExportProgressStartDate('');
|
||||||
setExportProgressEndDate('');
|
setExportProgressEndDate('');
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const exportProgressStartDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
|
const exportProgressStartDateChangeHandler: ChangeEventHandler<
|
||||||
useCallback((e) => {
|
HTMLInputElement
|
||||||
setExportProgressStartDate(e.target.value);
|
> = (e) => {
|
||||||
}, []);
|
setExportProgressStartDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const exportProgressEndDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
|
const exportProgressEndDateChangeHandler: ChangeEventHandler<
|
||||||
useCallback((e) => {
|
HTMLInputElement
|
||||||
setExportProgressEndDate(e.target.value);
|
> = (e) => {
|
||||||
}, []);
|
setExportProgressEndDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const exportProgressInputToExcelClickHandler = useCallback(() => {
|
const exportProgressInputToExcelClickHandler = () => {
|
||||||
resetExportProgressForm();
|
resetExportProgressForm();
|
||||||
exportProgressInputModal.openModal();
|
exportProgressInputModal.openModal();
|
||||||
}, [exportProgressInputModal, resetExportProgressForm]);
|
};
|
||||||
|
|
||||||
const submitExportProgressInputHandler = useCallback(async () => {
|
const submitExportProgressInputHandler = async () => {
|
||||||
if (!exportProgressStartDate || !exportProgressEndDate) {
|
if (!exportProgressStartDate || !exportProgressEndDate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -585,12 +611,7 @@ const MarketingTable = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsExportProgressLoading(false);
|
setIsExportProgressLoading(false);
|
||||||
}
|
}
|
||||||
}, [
|
};
|
||||||
exportProgressEndDate,
|
|
||||||
exportProgressInputModal,
|
|
||||||
exportProgressStartDate,
|
|
||||||
resetExportProgressForm,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
||||||
return [
|
return [
|
||||||
@@ -656,7 +677,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'so_do_number',
|
accessorKey: 'so_number',
|
||||||
header: 'No. Order',
|
header: 'No. Order',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
return props.row.original.do_number
|
return props.row.original.do_number
|
||||||
@@ -672,7 +693,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'approval.step_name',
|
accessorKey: 'status',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
@@ -707,10 +728,12 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'customer.name',
|
accessorKey: 'customer',
|
||||||
header: 'Customer',
|
header: 'Customer',
|
||||||
|
cell: (props) => props.row.original.customer.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'grand_total',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.sales_order
|
row.sales_order
|
||||||
?.map((product) => product.total_price)
|
?.map((product) => product.total_price)
|
||||||
@@ -727,6 +750,7 @@ const MarketingTable = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'marketing_products.length',
|
accessorKey: 'marketing_products.length',
|
||||||
header: 'Product Details',
|
header: 'Product Details',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
if (props?.row?.original?.sales_order?.length) {
|
if (props?.row?.original?.sales_order?.length) {
|
||||||
if (props?.row?.original?.sales_order?.length > 1) {
|
if (props?.row?.original?.sales_order?.length > 1) {
|
||||||
@@ -949,6 +973,8 @@ const MarketingTable = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
pageSize={tableFilterState.pageSize}
|
pageSize={tableFilterState.pageSize}
|
||||||
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={handleSortingChange}
|
||||||
totalItems={
|
totalItems={
|
||||||
isResponseSuccess(marketing)
|
isResponseSuccess(marketing)
|
||||||
? marketing?.meta?.total_results
|
? marketing?.meta?.total_results
|
||||||
|
|||||||
Reference in New Issue
Block a user