Merge branch 'feat/marketing-filter-range-date' into 'rc/00'

feat: add date range, filter by, and warehouse filter to marketing table

See merge request mbugroup/lti-web-client!504
This commit is contained in:
Giovanni Gabriel Septriadi
2026-06-04 16:52:49 +00:00
4 changed files with 189 additions and 15 deletions
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { RefObject, useCallback, useMemo } from 'react'; import { RefObject, useCallback, useMemo, useState } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
@@ -9,6 +9,8 @@ import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
} from '@/components/input/SelectInput'; } 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 { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
import { import {
MarketingFilterFormValues, MarketingFilterFormValues,
@@ -17,12 +19,17 @@ import {
import { MarketingFilter } from '@/types/api/marketing/marketing'; import { MarketingFilter } from '@/types/api/marketing/marketing';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { MarketingApi } from '@/services/api/marketing/marketing'; 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 { isResponseSuccess } from '@/lib/api-helper';
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing'; import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
import { ProjectFlockApi } from '@/services/api/production'; import { ProjectFlockApi } from '@/services/api/production';
import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlock } from '@/types/api/production/project-flock';
import { Product } from '@/types/api/master-data/product'; import { Product } from '@/types/api/master-data/product';
import { Warehouse } from '@/types/api/master-data/warehouse';
interface MarketingFilterModal { interface MarketingFilterModal {
ref: RefObject<HTMLDialogElement | null>; ref: RefObject<HTMLDialogElement | null>;
@@ -34,6 +41,10 @@ interface MarketingFilterModal {
customer: OptionType<number> | null; customer: OptionType<number> | null;
project_flock: OptionType<number> | null; project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null; project_flock_kandang: OptionType<number> | null;
warehouse: OptionType<number> | null;
start_date: string;
end_date: string;
filter_by: OptionType<string> | null;
}; };
} }
@@ -79,6 +90,13 @@ const MarketingFilterModal = ({
'search' 'search'
); );
const {
options: warehouseOptions,
isLoadingOptions: isLoadingWarehouseOptions,
setInputValue: setWarehouseInputValue,
loadMore: loadMoreWarehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search');
const statusOptions = [ const statusOptions = [
...MARKETING_APPROVAL_LINE.map((item) => ({ ...MARKETING_APPROVAL_LINE.map((item) => ({
value: item.step_name.split(' ').join('_').toUpperCase(), value: item.step_name.split(' ').join('_').toUpperCase(),
@@ -87,6 +105,13 @@ const MarketingFilterModal = ({
{ value: 'DITOLAK', label: 'Ditolak' }, { 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<MarketingFilterFormValues>({ const formik = useFormik<MarketingFilterFormValues>({
initialValues: initialValues || { initialValues: initialValues || {
product_ids: [], product_ids: [],
@@ -94,6 +119,10 @@ const MarketingFilterModal = ({
customer: null, customer: null,
project_flock: null, project_flock: null,
project_flock_kandang: null, project_flock_kandang: null,
warehouse: null,
start_date: '',
end_date: '',
filter_by: null,
}, },
validationSchema: MarketingFilterSchema, validationSchema: MarketingFilterSchema,
@@ -111,6 +140,12 @@ const MarketingFilterModal = ({
Number(values.project_flock_kandang?.value) || undefined, Number(values.project_flock_kandang?.value) || undefined,
project_flock_kandang_name: project_flock_kandang_name:
values.project_flock_kandang?.label || undefined, 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); onSubmit?.(formattedValues);
@@ -133,12 +168,37 @@ const MarketingFilterModal = ({
customer: null, customer: null,
project_flock: null, project_flock: null,
project_flock_kandang: null, project_flock_kandang: null,
warehouse: null,
start_date: '',
end_date: '',
filter_by: null,
}, },
}); });
setHasDateError(false);
onReset?.(); onReset?.();
closeModalHandler(); closeModalHandler();
}, [resetForm, onReset, closeModalHandler]); }, [resetForm, onReset, closeModalHandler]);
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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) => { const productChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('product_ids', val as OptionType[]); formik.setFieldValue('product_ids', val as OptionType[]);
}; };
@@ -207,6 +267,44 @@ const MarketingFilterModal = ({
{/* Modal Body */} {/* Modal Body */}
<div className='p-4 flex flex-col gap-1.5'> <div className='p-4 flex flex-col gap-1.5'>
<div>
<label className='block text-xs font-semibold text-base-content py-2'>
Tanggal
</label>
<div className='flex flex-row gap-1.5 items-center justify-between'>
<DateInput
name='start_date'
value={formik.values.start_date || ''}
onChange={handleStartDateChange}
className={{ wrapper: 'w-full' }}
isNestedModal
/>
<hr className='w-full max-w-3 h-px border-base-content/10' />
<DateInput
name='end_date'
value={formik.values.end_date || ''}
onChange={handleEndDateChange}
className={{ wrapper: 'w-full' }}
isNestedModal
isError={hasDateError}
/>
</div>
</div>
<SelectInputRadio
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={filterByOptions}
value={formik.values.filter_by ?? null}
onChange={(val) =>
formik.setFieldValue(
'filter_by',
!Array.isArray(val) ? (val ?? null) : null
)
}
isClearable
/>
{/* select multiple product */} {/* select multiple product */}
<SelectInputCheckbox <SelectInputCheckbox
label='Product' label='Product'
@@ -272,6 +370,22 @@ const MarketingFilterModal = ({
} }
isDisabled={!formik.values.project_flock} isDisabled={!formik.values.project_flock}
/> />
<SelectInput
label='Gudang'
isClearable
placeholder='Pilih Gudang'
options={warehouseOptions}
isLoading={isLoadingWarehouseOptions}
value={formik.values.warehouse}
onChange={(val) =>
formik.setFieldValue(
'warehouse',
!Array.isArray(val) ? (val as OptionType<number> | null) : null
)
}
onInputChange={setWarehouseInputValue}
onMenuScrollToBottom={loadMoreWarehouses}
/>
</div> </div>
{/* Modal Footer */} {/* Modal Footer */}
@@ -288,6 +402,7 @@ const MarketingFilterModal = ({
<Button <Button
type='submit' type='submit'
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm' className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
disabled={hasDateError}
> >
Apply Filter Apply Filter
</Button> </Button>
@@ -203,6 +203,12 @@ const MarketingTable = () => {
project_flock_name: '', project_flock_name: '',
project_flock_kandang_id: '', project_flock_kandang_id: '',
project_flock_kandang_name: '', project_flock_kandang_name: '',
warehouse_id: '',
warehouse_name: '',
start_date: '',
end_date: '',
filter_by: '',
filter_by_name: '',
sort_by: '', sort_by: '',
order_by: '', order_by: '',
}, },
@@ -214,6 +220,10 @@ 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',
warehouse_id: 'warehouse_id',
start_date: 'start_date',
end_date: 'end_date',
filter_by: 'filter_by',
sort_by: 'sort_by', sort_by: 'sort_by',
order_by: 'sort_order', order_by: 'sort_order',
}, },
@@ -223,6 +233,8 @@ const MarketingTable = () => {
'customer_name', 'customer_name',
'project_flock_name', 'project_flock_name',
'project_flock_kandang_name', 'project_flock_kandang_name',
'warehouse_name',
'filter_by_name',
], ],
persist: true, persist: true,
@@ -293,6 +305,16 @@ const MarketingTable = () => {
values.project_flock_kandang_name ?? '', values.project_flock_kandang_name ?? '',
true 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] = const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
@@ -311,6 +333,12 @@ const MarketingTable = () => {
updateFilter('project_flock_name', '', true); updateFilter('project_flock_name', '', true);
updateFilter('project_flock_kandang_id', '', true); updateFilter('project_flock_kandang_id', '', true);
updateFilter('project_flock_kandang_name', '', 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 = () => { const approveClickHandler = () => {
@@ -433,6 +461,20 @@ const MarketingTable = () => {
label: tableFilterState.project_flock_kandang_name, label: tableFilterState.project_flock_kandang_name,
} }
: null, : 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) => { const approveMarketingHandler = async (notes: string) => {
@@ -707,7 +749,7 @@ const MarketingTable = () => {
}, },
{ {
accessorKey: 'so_date', accessorKey: 'so_date',
header: 'Tanggal', header: 'Tanggal SO',
cell: (props) => { cell: (props) => {
return formatDate(props.row.original.so_date, 'DD MMM yyyy'); return formatDate(props.row.original.so_date, 'DD MMM yyyy');
}, },
@@ -753,18 +795,17 @@ const MarketingTable = () => {
cell: (props) => props.row.original.customer.name, cell: (props) => props.row.original.customer.name,
}, },
{ {
accessorKey: 'grand_total', accessorKey: 'grand_total_so',
accessorFn: (row) => header: 'Grand Total SO',
row.sales_order
?.map((product) => product.total_price)
.reduce((a, b) => a + b, 0) ?? 0,
header: 'Grand Total',
cell: (props) => { cell: (props) => {
return formatCurrency( return formatCurrency(props.row.original?.grand_total_so);
props.row.original?.sales_order },
?.map((product) => product.total_price) },
.reduce((a, b) => a + b, 0) ?? 0 {
); 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', 'customer_name',
'project_flock_name', 'project_flock_name',
'project_flock_kandang_name', 'project_flock_kandang_name',
'warehouse_name',
'filter_by_name',
'sort_by', 'sort_by',
'order_by', 'order_by',
]} ]}
@@ -1,4 +1,4 @@
import { array, mixed, object } from 'yup'; import { array, mixed, object, string } from 'yup';
import { OptionType } from '@/components/input/SelectInput'; import { OptionType } from '@/components/input/SelectInput';
export const MarketingFilterSchema = object({ export const MarketingFilterSchema = object({
@@ -7,6 +7,10 @@ export const MarketingFilterSchema = object({
customer: mixed<OptionType<number>>().nullable(), customer: mixed<OptionType<number>>().nullable(),
project_flock: mixed<OptionType<number>>().nullable(), project_flock: mixed<OptionType<number>>().nullable(),
project_flock_kandang: mixed<OptionType<number>>().nullable(), project_flock_kandang: mixed<OptionType<number>>().nullable(),
warehouse: mixed<OptionType<number>>().nullable(),
start_date: string().optional(),
end_date: string().optional(),
filter_by: mixed<OptionType<string>>().nullable(),
}); });
export type MarketingFilterFormValues = { export type MarketingFilterFormValues = {
@@ -15,4 +19,8 @@ export type MarketingFilterFormValues = {
customer: OptionType<number> | null; customer: OptionType<number> | null;
project_flock: OptionType<number> | null; project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null; project_flock_kandang: OptionType<number> | null;
warehouse: OptionType<number> | null;
start_date: string;
end_date: string;
filter_by: OptionType<string> | null;
}; };
+8
View File
@@ -23,6 +23,8 @@ export type BaseMarketing = {
latest_approval: BaseApproval; latest_approval: BaseApproval;
sales_order: BaseSalesOrder[]; sales_order: BaseSalesOrder[];
delivery_order: BaseDeliveryOrder[]; delivery_order: BaseDeliveryOrder[];
grand_total_do: number;
grand_total_so: number;
}; };
export type BaseSalesOrder = { export type BaseSalesOrder = {
@@ -104,6 +106,12 @@ export type MarketingFilter = {
project_flock_name?: string; project_flock_name?: string;
project_flock_kandang_id?: number; project_flock_kandang_id?: number;
project_flock_kandang_name?: string; project_flock_kandang_name?: string;
start_date?: string;
end_date?: string;
filter_by?: string;
filter_by_name?: string;
warehouse_id?: number;
warehouse_name?: string;
}; };
/** /**