From e73af7e252390c9c7ae46454ad14879d59e82929 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 2 Jun 2026 13:22:01 +0700 Subject: [PATCH] feat: add date range, filter by, and warehouse filter to marketing table - Add start_date and end_date range inputs to the marketing filter modal with validation that prevents end date from being earlier than start date - Add 'Filter Berdasarkan' single-select radio (so_date / created_at) to let users choose which date field the range applies to - Add single-select Gudang (warehouse) filter backed by WarehouseApi, serialized as warehouse_id query param - Wire all three new filters into useTableFilter (paramMap, persist, excludeKeysFromUrl for label-only fields) and propagate through filterSubmitHandler, filterResetHandler, and marketingFilterInitialValues so filter state survives page refreshes Co-Authored-By: Claude Sonnet 4.6 --- .../pages/marketing/MarketingFilter.tsx | 119 +++++++++++++++++- .../pages/marketing/MarketingTable.tsx | 67 ++++++++-- .../pages/marketing/filter/MarketingFilter.ts | 10 +- src/types/api/marketing/marketing.d.ts | 8 ++ 4 files changed, 189 insertions(+), 15 deletions(-) diff --git a/src/components/pages/marketing/MarketingFilter.tsx b/src/components/pages/marketing/MarketingFilter.tsx index 8e1ab8c0..20b4bbd3 100644 --- a/src/components/pages/marketing/MarketingFilter.tsx +++ b/src/components/pages/marketing/MarketingFilter.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject, useCallback, useMemo } from 'react'; +import { RefObject, useCallback, useMemo, useState } from 'react'; import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import Modal from '@/components/Modal'; @@ -9,6 +9,8 @@ import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; import { MarketingFilterFormValues, @@ -17,12 +19,17 @@ import { import { MarketingFilter } from '@/types/api/marketing/marketing'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import { MarketingApi } from '@/services/api/marketing/marketing'; -import { CustomerApi, ProductApi } from '@/services/api/master-data'; +import { + CustomerApi, + ProductApi, + WarehouseApi, +} from '@/services/api/master-data'; import { isResponseSuccess } from '@/lib/api-helper'; import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing'; import { ProjectFlockApi } from '@/services/api/production'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { Product } from '@/types/api/master-data/product'; +import { Warehouse } from '@/types/api/master-data/warehouse'; interface MarketingFilterModal { ref: RefObject; @@ -34,6 +41,10 @@ interface MarketingFilterModal { customer: OptionType | null; project_flock: OptionType | null; project_flock_kandang: OptionType | null; + warehouse: OptionType | null; + start_date: string; + end_date: string; + filter_by: OptionType | null; }; } @@ -79,6 +90,13 @@ const MarketingFilterModal = ({ 'search' ); + const { + options: warehouseOptions, + isLoadingOptions: isLoadingWarehouseOptions, + setInputValue: setWarehouseInputValue, + loadMore: loadMoreWarehouses, + } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); + const statusOptions = [ ...MARKETING_APPROVAL_LINE.map((item) => ({ value: item.step_name.split(' ').join('_').toUpperCase(), @@ -87,6 +105,13 @@ const MarketingFilterModal = ({ { value: 'DITOLAK', label: 'Ditolak' }, ]; + const filterByOptions = [ + { value: 'so_date', label: 'Tanggal SO' }, + { value: 'created_at', label: 'Tanggal Dibuat' }, + ]; + + const [hasDateError, setHasDateError] = useState(false); + const formik = useFormik({ initialValues: initialValues || { product_ids: [], @@ -94,6 +119,10 @@ const MarketingFilterModal = ({ customer: null, project_flock: null, project_flock_kandang: null, + warehouse: null, + start_date: '', + end_date: '', + filter_by: null, }, validationSchema: MarketingFilterSchema, @@ -111,6 +140,12 @@ const MarketingFilterModal = ({ Number(values.project_flock_kandang?.value) || undefined, project_flock_kandang_name: values.project_flock_kandang?.label || undefined, + warehouse_id: Number(values.warehouse?.value) || undefined, + warehouse_name: values.warehouse?.label || undefined, + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + filter_by: values.filter_by?.value || undefined, + filter_by_name: values.filter_by?.label || undefined, }; onSubmit?.(formattedValues); @@ -133,12 +168,37 @@ const MarketingFilterModal = ({ customer: null, project_flock: null, project_flock_kandang: null, + warehouse: null, + start_date: '', + end_date: '', + filter_by: null, }, }); + setHasDateError(false); onReset?.(); closeModalHandler(); }, [resetForm, onReset, closeModalHandler]); + const handleStartDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('start_date', value); + if (value && formik.values.end_date) { + setHasDateError(new Date(formik.values.end_date) < new Date(value)); + } else { + setHasDateError(false); + } + }; + + const handleEndDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('end_date', value); + if (value && formik.values.start_date) { + setHasDateError(new Date(value) < new Date(formik.values.start_date)); + } else { + setHasDateError(false); + } + }; + const productChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldValue('product_ids', val as OptionType[]); }; @@ -207,6 +267,44 @@ const MarketingFilterModal = ({ {/* Modal Body */}
+
+ +
+ +
+ +
+
+ + + formik.setFieldValue( + 'filter_by', + !Array.isArray(val) ? (val ?? null) : null + ) + } + isClearable + /> + {/* select multiple product */} + + formik.setFieldValue( + 'warehouse', + !Array.isArray(val) ? (val as OptionType | null) : null + ) + } + onInputChange={setWarehouseInputValue} + onMenuScrollToBottom={loadMoreWarehouses} + />
{/* Modal Footer */} @@ -288,6 +402,7 @@ const MarketingFilterModal = ({ diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 39384b0d..4f09642f 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -203,6 +203,12 @@ const MarketingTable = () => { project_flock_name: '', project_flock_kandang_id: '', project_flock_kandang_name: '', + warehouse_id: '', + warehouse_name: '', + start_date: '', + end_date: '', + filter_by: '', + filter_by_name: '', sort_by: '', order_by: '', }, @@ -214,6 +220,10 @@ const MarketingTable = () => { customer_id: 'customer_id', project_flock_id: 'project_flock_id', project_flock_kandang_id: 'project_flock_kandang_id', + warehouse_id: 'warehouse_id', + start_date: 'start_date', + end_date: 'end_date', + filter_by: 'filter_by', sort_by: 'sort_by', order_by: 'sort_order', }, @@ -223,6 +233,8 @@ const MarketingTable = () => { 'customer_name', 'project_flock_name', 'project_flock_kandang_name', + 'warehouse_name', + 'filter_by_name', ], persist: true, @@ -293,6 +305,16 @@ const MarketingTable = () => { values.project_flock_kandang_name ?? '', true ); + updateFilter( + 'warehouse_id', + values.warehouse_id ? values.warehouse_id.toString() : '', + true + ); + updateFilter('warehouse_name', values.warehouse_name ?? '', true); + updateFilter('start_date', values.start_date ?? '', true); + updateFilter('end_date', values.end_date ?? '', true); + updateFilter('filter_by', values.filter_by ?? '', true); + updateFilter('filter_by_name', values.filter_by_name ?? '', true); }; const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = @@ -311,6 +333,12 @@ const MarketingTable = () => { updateFilter('project_flock_name', '', true); updateFilter('project_flock_kandang_id', '', true); updateFilter('project_flock_kandang_name', '', true); + updateFilter('warehouse_id', '', true); + updateFilter('warehouse_name', '', true); + updateFilter('start_date', '', true); + updateFilter('end_date', '', true); + updateFilter('filter_by', '', true); + updateFilter('filter_by_name', '', true); }; const approveClickHandler = () => { @@ -433,6 +461,20 @@ const MarketingTable = () => { label: tableFilterState.project_flock_kandang_name, } : null, + warehouse: tableFilterState.warehouse_id + ? { + value: Number(tableFilterState.warehouse_id), + label: tableFilterState.warehouse_name, + } + : null, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, + filter_by: tableFilterState.filter_by + ? { + value: tableFilterState.filter_by, + label: tableFilterState.filter_by_name, + } + : null, }; const approveMarketingHandler = async (notes: string) => { @@ -707,7 +749,7 @@ const MarketingTable = () => { }, { accessorKey: 'so_date', - header: 'Tanggal', + header: 'Tanggal SO', cell: (props) => { return formatDate(props.row.original.so_date, 'DD MMM yyyy'); }, @@ -753,18 +795,17 @@ const MarketingTable = () => { cell: (props) => props.row.original.customer.name, }, { - accessorKey: 'grand_total', - accessorFn: (row) => - row.sales_order - ?.map((product) => product.total_price) - .reduce((a, b) => a + b, 0) ?? 0, - header: 'Grand Total', + accessorKey: 'grand_total_so', + header: 'Grand Total SO', cell: (props) => { - return formatCurrency( - props.row.original?.sales_order - ?.map((product) => product.total_price) - .reduce((a, b) => a + b, 0) ?? 0 - ); + return formatCurrency(props.row.original?.grand_total_so); + }, + }, + { + accessorKey: 'grand_total_do', + header: 'Grand Total DO', + cell: (props) => { + return formatCurrency(props.row.original?.grand_total_do); }, }, { @@ -911,6 +952,8 @@ const MarketingTable = () => { 'customer_name', 'project_flock_name', 'project_flock_kandang_name', + 'warehouse_name', + 'filter_by_name', 'sort_by', 'order_by', ]} diff --git a/src/components/pages/marketing/filter/MarketingFilter.ts b/src/components/pages/marketing/filter/MarketingFilter.ts index a306d89d..4b9ee07b 100644 --- a/src/components/pages/marketing/filter/MarketingFilter.ts +++ b/src/components/pages/marketing/filter/MarketingFilter.ts @@ -1,4 +1,4 @@ -import { array, mixed, object } from 'yup'; +import { array, mixed, object, string } from 'yup'; import { OptionType } from '@/components/input/SelectInput'; export const MarketingFilterSchema = object({ @@ -7,6 +7,10 @@ export const MarketingFilterSchema = object({ customer: mixed>().nullable(), project_flock: mixed>().nullable(), project_flock_kandang: mixed>().nullable(), + warehouse: mixed>().nullable(), + start_date: string().optional(), + end_date: string().optional(), + filter_by: mixed>().nullable(), }); export type MarketingFilterFormValues = { @@ -15,4 +19,8 @@ export type MarketingFilterFormValues = { customer: OptionType | null; project_flock: OptionType | null; project_flock_kandang: OptionType | null; + warehouse: OptionType | null; + start_date: string; + end_date: string; + filter_by: OptionType | null; }; diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index ee3a95d3..909e2af4 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -23,6 +23,8 @@ export type BaseMarketing = { latest_approval: BaseApproval; sales_order: BaseSalesOrder[]; delivery_order: BaseDeliveryOrder[]; + grand_total_do: number; + grand_total_so: number; }; export type BaseSalesOrder = { @@ -104,6 +106,12 @@ export type MarketingFilter = { project_flock_name?: string; project_flock_kandang_id?: number; project_flock_kandang_name?: string; + start_date?: string; + end_date?: string; + filter_by?: string; + filter_by_name?: string; + warehouse_id?: number; + warehouse_name?: string; }; /**