From e73af7e252390c9c7ae46454ad14879d59e82929 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 2 Jun 2026 13:22:01 +0700 Subject: [PATCH 1/4] 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; }; /** From 97acc17ca55e6d60e3c1ee11097eb0bee829647d Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 3 Jun 2026 14:05:06 +0700 Subject: [PATCH 2/4] feat: add inline edit for chick-in date in chickin logs - Add updateChickinDate method to ChickinService (PATCH /production/chickins/chick-in-date) - Add pencil icon button next to each chickin date in ChickLogsView - Clicking the icon toggles an inline DateInput with Simpan/Batal buttons - Save button is disabled and shows loading state while request is in flight Co-Authored-By: Claude Sonnet 4.6 --- .../chickin/form/tabs/ChickLogsView.tsx | 74 ++++++++++++++++++- src/services/api/production/chickin.ts | 24 ++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index acb8c18b..6fbde87a 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -2,16 +2,17 @@ import Alert from '@/components/Alert'; import Button from '@/components/Button'; import Card from '@/components/Card'; import RequirePermission from '@/components/helper/RequirePermission'; +import DateInput from '@/components/input/DateInput'; import PillBadge from '@/components/PillBadge'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; import { ChickinApi } from '@/services/api/production/chickin'; +import { useChickinStore } from '@/stores/production/chickin/chickin.store'; import { BaseApproval } from '@/types/api/api-general'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { Icon } from '@iconify/react'; import { useState } from 'react'; import toast from 'react-hot-toast'; -import { useChickinStore } from '@/stores/production/chickin/chickin.store'; const ChickinLogsView = ({ initialValues, @@ -23,6 +24,9 @@ const ChickinLogsView = ({ rawDataApprovals: BaseApproval[]; }) => { const [chickinErrorMessage, setChickinErrorMessage] = useState(''); + const [editingChickinId, setEditingChickinId] = useState(null); + const [editDate, setEditDate] = useState(''); + const [isEditLoading, setIsEditLoading] = useState(false); const { openChickinApproveModal, openChickinDeleteModal } = useChickinStore(); const handleClickApprove = () => { @@ -44,6 +48,23 @@ const ChickinLogsView = ({ }); }; + const handleSaveChickinDate = async () => { + setIsEditLoading(true); + const res = await ChickinApi.updateChickinDate( + initialValues.id as number, + formatDate(editDate, 'YYYY-MM-DD') + ); + setIsEditLoading(false); + if (isResponseSuccess(res)) { + toast.success(res?.message as string); + setEditingChickinId(null); + afterSubmit && afterSubmit(); + } + if (isResponseError(res)) { + toast.error(res?.message as string); + } + }; + const handleDeleteChickin = (chickinId: number) => { openChickinDeleteModal(chickinId, async () => { const deleteRes = await ChickinApi.delete(chickinId); @@ -133,9 +154,54 @@ const ChickinLogsView = ({ {' '} Tanggal Chick In -
- {formatDate(chickin.chick_in_date, 'DD MMM YYYY')} -
+ {editingChickinId === chickin.id ? ( +
+ setEditDate(e.target.value)} + /> +
+ + +
+
+ ) : ( +
+ + {formatDate(chickin.chick_in_date, 'DD MMM YYYY')} + + +
+ )} {/* Kandang */} diff --git a/src/services/api/production/chickin.ts b/src/services/api/production/chickin.ts index 0efaa0f9..d250e450 100644 --- a/src/services/api/production/chickin.ts +++ b/src/services/api/production/chickin.ts @@ -6,6 +6,7 @@ import { import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { httpClient } from '@/services/http/client'; +import axios from 'axios'; export class ChickinService extends BaseApiService< Chickin, @@ -16,6 +17,29 @@ export class ChickinService extends BaseApiService< super(basePath); } + async updateChickinDate( + projectFlockKandangId: number, + chickInDate: string + ): Promise | undefined> { + try { + return await httpClient>( + `${this.basePath}/chick-in-date`, + { + method: 'PATCH', + body: { + project_flock_kandang_id: projectFlockKandangId, + chick_in_date: chickInDate, + }, + } + ); + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } + /** * Approve single marketing data */ From f167916a21df70d401b473a0354510c05d0de77a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 3 Jun 2026 14:20:43 +0700 Subject: [PATCH 3/4] fix: replace throw error with axios error handling in SalesOrderService and MarketingExportService All catch blocks in singleApproval, bulkApprovals (both classes), and delivery now return error.response?.data for axios errors and undefined otherwise, consistent with the BaseApiService pattern. Co-Authored-By: Claude Sonnet 4.6 --- src/services/api/marketing/marketing.ts | 28 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/services/api/marketing/marketing.ts b/src/services/api/marketing/marketing.ts index 2cd225a5..4ded8cc2 100644 --- a/src/services/api/marketing/marketing.ts +++ b/src/services/api/marketing/marketing.ts @@ -45,8 +45,11 @@ export class SalesOrderService extends BaseApiService< notes: notes || `${action} marketing ${id}`, }, }); - } catch (error) { - throw error; + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; } } @@ -68,8 +71,11 @@ export class SalesOrderService extends BaseApiService< notes: notes || `${action} marketing ${ids.join(', ')}`, }, }); - } catch (error) { - throw error; + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; } } @@ -110,8 +116,11 @@ export class SalesOrderService extends BaseApiService< notes: notes || `Delivery marketing ${id}`, }, }); - } catch (error) { - throw error; + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; } } } @@ -142,8 +151,11 @@ class MarketingExportService extends BaseApiService< notes: notes, }, }); - } catch (error) { - throw error; + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; } } From 4151829cb8afe77fd6f34b6b38c6b6eb57859ddf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 3 Jun 2026 14:24:39 +0700 Subject: [PATCH 4/4] fix: disabled deliver item button if is submitting and set the is loading prop --- src/components/pages/marketing/DeliveryOrderFormModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index fe98603d..89783a9e 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -847,7 +847,8 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { } }} className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold' - disabled={deliveryRejected} + disabled={deliveryRejected || isLoading} + isLoading={isLoading} > {marketing?.data?.latest_approval?.step_number === 1 && 'Approve'}