From 75e9b06a83b95105b63ed80a76f28fb068c57269 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 23 Feb 2026 10:27:34 +0700 Subject: [PATCH 001/149] refactor(FE): Set default value for `show_unrecorded` to `false` --- .../pages/report/marketing/tab/HppPerKandangTab.tsx | 11 +++++++---- src/services/api/report/marketing-sale.ts | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 991c6546..4dee73f8 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -110,7 +110,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { weight_max: null, period: null, sort_by: null, - show_unrecorded: null, + show_unrecorded: false, }, validationSchema: HppPerKandangFilterSchema, onSubmit: (values, { setSubmitting }) => { @@ -122,8 +122,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { weight_max: values.weight_max || undefined, period: values.period || undefined, sort_by: values.sort_by || undefined, - show_unrecorded: - values.show_unrecorded !== null ? values.show_unrecorded : undefined, + show_unrecorded: values.show_unrecorded ?? undefined, }); filterModal.closeModal(); setIsSubmitted(true); @@ -1026,7 +1025,11 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { if (!Array.isArray(val)) { formik.setFieldValue( 'show_unrecorded', - val?.value === 'true' || null + val?.value === 'true' + ? true + : val?.value === 'false' + ? false + : null ); } }} diff --git a/src/services/api/report/marketing-sale.ts b/src/services/api/report/marketing-sale.ts index 92c59b2c..f474adb7 100644 --- a/src/services/api/report/marketing-sale.ts +++ b/src/services/api/report/marketing-sale.ts @@ -35,7 +35,7 @@ export class MarketingSaleReportService extends BaseApiService< weight_max: weight_max, period: period, sort_by: sort_by, - show_unrecorded: show_unrecorded, + show_unrecorded: show_unrecorded ?? false, page: page, limit: limit, }, From 7e1166b5e8edb2a8ef625fa02a0aeeb56f434139 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 23 Feb 2026 11:05:11 +0700 Subject: [PATCH 002/149] refactor(FE): Refactor tab actions store to use a unified implementation --- .../pages/closing/ClosingDetailTabs.tsx | 4 +- .../report/expense/ReportExpenseTabs.tsx | 4 +- .../report/expense/tab/ReportExpenseTab.tsx | 6 +-- .../pages/report/finance/FinanceTabs.tsx | 4 +- .../report/finance/tab/CustomerPaymentTab.tsx | 6 +-- .../report/finance/tab/DebtSupplierTab.tsx | 6 +-- .../logistic-stock/LogisticStockTabs.tsx | 4 +- .../tab/PurchasesPerSupplierTab.tsx | 6 +-- .../pages/report/marketing/MarketingTabs.tsx | 4 +- .../marketing/tab/DailyMarketingTab.tsx | 6 +-- .../report/marketing/tab/HppPerKandangTab.tsx | 6 +-- .../ProductionResultTabs.tsx | 5 +-- ...ProductionResultProjectFlockKandangTab.tsx | 6 +-- src/stores/closing/closing-tab.store.ts | 21 ---------- .../closing/slices/closing-tab.slice.ts | 37 ---------------- src/stores/report/report-tab.store.ts | 21 ---------- src/stores/report/slices/report-tab.slice.ts | 37 ---------------- src/stores/tab-actions/tab-actions.store.ts | 42 +++++++++++++++++++ 18 files changed, 75 insertions(+), 150 deletions(-) delete mode 100644 src/stores/closing/closing-tab.store.ts delete mode 100644 src/stores/closing/slices/closing-tab.slice.ts delete mode 100644 src/stores/report/report-tab.store.ts delete mode 100644 src/stores/report/slices/report-tab.slice.ts create mode 100644 src/stores/tab-actions/tab-actions.store.ts diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index dc8bd6f8..12651313 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -18,7 +18,7 @@ import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditio import ClosingKandangList from '@/components/pages/closing/ClosingKandangList'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -import { useClosingTabStore } from '@/stores/closing/closing-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; @@ -33,7 +33,7 @@ const ClosingDetail: React.FC = ({ kandangData, }) => { const [activeTabId, setActiveTabId] = useState('sapronak'); - const tabActions = useClosingTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const closingDetailTabs = useMemo(() => { const validTabs = [ diff --git a/src/components/pages/report/expense/ReportExpenseTabs.tsx b/src/components/pages/report/expense/ReportExpenseTabs.tsx index 704d1f6f..0bde153d 100644 --- a/src/components/pages/report/expense/ReportExpenseTabs.tsx +++ b/src/components/pages/report/expense/ReportExpenseTabs.tsx @@ -3,12 +3,12 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import ReportExpenseTab from './tab/ReportExpenseTab'; const ReportExpenseTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index 2581ec5c..121338b7 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -19,7 +19,7 @@ import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { ReportExpense } from '@/types/api/report/report-expense'; import { ReportExpenseApi } from '@/services/api/report'; import { isResponseSuccess } from '@/lib/api-helper'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import Modal, { useModal } from '@/components/Modal'; import Pagination from '@/components/Pagination'; import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton'; @@ -305,8 +305,8 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index de924f62..5c49ed3c 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -4,11 +4,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; const FinanceTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 1c546058..9ee90fae 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -34,7 +34,7 @@ import { } from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { OptionType } from '@/components/table/TableRowSizeSelector'; import { Color } from '@/types/theme'; @@ -373,8 +373,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { }, [customerPaymentExport, filterParams, customerOptions]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 635bece8..d1b7425d 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -31,7 +31,7 @@ import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import StatusBadge from '@/components/helper/StatusBadge'; import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; @@ -265,8 +265,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }, [debtSupplierExport]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index 1e3f4109..5e292afa 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -3,11 +3,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; const LogisticStockTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 87c5ee8d..6f1599bc 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -30,7 +30,7 @@ import { } from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; interface PurchasesPerSupplierTabProps { @@ -479,8 +479,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/marketing/MarketingTabs.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx index 8a02a0c2..87139574 100644 --- a/src/components/pages/report/marketing/MarketingTabs.tsx +++ b/src/components/pages/report/marketing/MarketingTabs.tsx @@ -4,11 +4,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; const MarketingReportContent = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index a336b671..eda89750 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -37,7 +37,7 @@ import { import SelectInput from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; import { useEffect as useEffectHook } from 'react'; import { httpClient } from '@/services/http/client'; @@ -390,8 +390,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }, [dailyMarketingsExport, summaryTotal]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffectHook(() => { setTabActions( diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 4dee73f8..dc487003 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -31,7 +31,7 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; import { useEffect as useEffectHook } from 'react'; @@ -479,8 +479,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffectHook(() => { setTabActions( diff --git a/src/components/pages/report/production-result/ProductionResultTabs.tsx b/src/components/pages/report/production-result/ProductionResultTabs.tsx index 6f5e4410..8e335cf2 100644 --- a/src/components/pages/report/production-result/ProductionResultTabs.tsx +++ b/src/components/pages/report/production-result/ProductionResultTabs.tsx @@ -3,11 +3,10 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; - +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; const ProductionResultTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useReportTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index 9ac5faf6..cbefadfe 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -34,7 +34,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { ProductionResult } from '@/types/api/report/production-result'; import ProductionResultReportPDF from '../export/ProductionResultExportPDF'; import { pdf } from '@react-pdf/renderer'; -import { useReportTabStore } from '@/stores/report/report-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import Modal, { useModal } from '@/components/Modal'; import { cn, formatNumber } from '@/lib/helper'; import Pagination from '@/components/Pagination'; @@ -532,8 +532,8 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { }, [filterParams]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useReportTabStore((state) => state.setTabActions); - const clearTabActions = useReportTabStore((state) => state.clearTabActions); + const setTabActions = useTabActionsStore((state) => state.setTabActions); + const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/stores/closing/closing-tab.store.ts b/src/stores/closing/closing-tab.store.ts deleted file mode 100644 index 1f81c26a..00000000 --- a/src/stores/closing/closing-tab.store.ts +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; - -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; -import { - createClosingTabSlice, - ClosingTabSlice, -} from '@/stores/closing/slices/closing-tab.slice'; - -export type ClosingTabStore = ClosingTabSlice; - -export const useClosingTabStore = create()( - devtools( - (...args) => ({ - ...createClosingTabSlice(...args), - }), - { - name: 'ClosingTabStore', - } - ) -); diff --git a/src/stores/closing/slices/closing-tab.slice.ts b/src/stores/closing/slices/closing-tab.slice.ts deleted file mode 100644 index cd47bbdc..00000000 --- a/src/stores/closing/slices/closing-tab.slice.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ReactNode } from 'react'; -import { StateCreator } from 'zustand'; - -export type ClosingTabSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const createClosingTabSlice: StateCreator< - ClosingTabSlice, - [], - [], - ClosingTabSlice -> = (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set((state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - })), - - clearTabActions: (tabId) => - set((state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }), - - clearAllTabActions: () => set({ tabActions: {} }), -}); diff --git a/src/stores/report/report-tab.store.ts b/src/stores/report/report-tab.store.ts deleted file mode 100644 index aad47d17..00000000 --- a/src/stores/report/report-tab.store.ts +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; - -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; -import { - createReportTabSlice, - ReportTabSlice, -} from '@/stores/report/slices/report-tab.slice'; - -export type ReportTabStore = ReportTabSlice; - -export const useReportTabStore = create()( - devtools( - (...args) => ({ - ...createReportTabSlice(...args), - }), - { - name: 'ReportTabStore', - } - ) -); diff --git a/src/stores/report/slices/report-tab.slice.ts b/src/stores/report/slices/report-tab.slice.ts deleted file mode 100644 index 6582eaed..00000000 --- a/src/stores/report/slices/report-tab.slice.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ReactNode } from 'react'; -import { StateCreator } from 'zustand'; - -export type ReportTabSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const createReportTabSlice: StateCreator< - ReportTabSlice, - [], - [], - ReportTabSlice -> = (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set((state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - })), - - clearTabActions: (tabId) => - set((state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }), - - clearAllTabActions: () => set({ tabActions: {} }), -}); diff --git a/src/stores/tab-actions/tab-actions.store.ts b/src/stores/tab-actions/tab-actions.store.ts new file mode 100644 index 00000000..15ccf186 --- /dev/null +++ b/src/stores/tab-actions/tab-actions.store.ts @@ -0,0 +1,42 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { ReactNode } from 'react'; + +export type TabActionsSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const useTabActionsStore = create()( + devtools( + (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set((state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + })), + + clearTabActions: (tabId) => + set((state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }), + + clearAllTabActions: () => set({ tabActions: {} }), + }), + { + name: 'TabActionsStore', + } + ) +); From 08aa1900a83a95dd74a8fc4a410e33e6a89080ed Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 23 Feb 2026 11:55:07 +0700 Subject: [PATCH 003/149] feat(FE): Add validation for disabling close button in ProjectFlockDetail --- .../detail/ProjectFlockDetail.tsx | 32 ++++++++++++++++--- src/types/api/production/project-flock.d.ts | 1 + 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index d3812e47..dd57a37f 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -1,6 +1,7 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput'; +import { useSelect } from '@/components/input/SelectInput'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; @@ -37,10 +38,36 @@ const ProjectFlockDetail = ({ null ); + const { rawData: projectFlockRawData } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + selectedKandangId ? { kandang_id: `[${selectedKandangId}]` } : undefined + ); + const selectedKandang = projectFlock.kandangs?.find( (kandang) => kandang.id === Number(selectedKandangId) ); + const hasActiveProjectWithSameKandang = isResponseSuccess(projectFlockRawData) + ? projectFlockRawData.data.some((pf) => + pf.kandangs?.some( + (k) => + k.id === Number(selectedKandangId) && + k.closed_at && + k.status !== 'NON_ACTIVE' + ) + ) + : false; + + const isCloseButtonDisabled = + !selectedKandangId || + projectFlock?.approval?.step_number == 1 || + (projectFlock?.category === 'GROWING' && + selectedKandang?.status === 'NON_ACTIVE' && + hasActiveProjectWithSameKandang); + const { data: projectFlockApprovalResponse } = useSWR( projectFlock.id ? ['approval-project-flock', projectFlock.id] : undefined, ([, id]) => ProjectFlockApi.getApprovalLineHistory(Number(id)) @@ -419,10 +446,7 @@ const ProjectFlockDetail = ({ className='w-full px-2 py-1 text-sm' variant='outline' color='error' - disabled={ - !selectedKandangId || - projectFlock?.approval?.step_number == 1 - } + disabled={isCloseButtonDisabled} > {selectedKandang?.status === 'NON_ACTIVE' ? ( <> diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 172c24b5..204e7b49 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -23,6 +23,7 @@ export type BaseProjectFlock = { kandang_ids: number[]; kandangs: (Kandang & { project_flock_kandang_id: number; + closed_at?: string; })[]; project_budgets?: ProjectFlockBudget[]; approval: BaseApproval; From 755bddc74c2a9366326b41bf2f9636d13dcf14e6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 23 Feb 2026 12:10:44 +0700 Subject: [PATCH 004/149] refactor(FE): Fix logic for checking active projects in the same kandang from the validation logic. --- .../production/project-flock/detail/ProjectFlockDetail.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index dd57a37f..cfa797d2 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -50,12 +50,14 @@ const ProjectFlockDetail = ({ (kandang) => kandang.id === Number(selectedKandangId) ); + // Cek apakah ada project aktif di kandang yang sama (selain project saat ini) + // Hanya boleh 1 kandang aktif, jadi jika ada project lain yang aktif, tidak bisa di-unclose baik kategori growing maupun laying const hasActiveProjectWithSameKandang = isResponseSuccess(projectFlockRawData) ? projectFlockRawData.data.some((pf) => pf.kandangs?.some( (k) => k.id === Number(selectedKandangId) && - k.closed_at && + pf.id !== projectFlock.id && k.status !== 'NON_ACTIVE' ) ) @@ -64,8 +66,7 @@ const ProjectFlockDetail = ({ const isCloseButtonDisabled = !selectedKandangId || projectFlock?.approval?.step_number == 1 || - (projectFlock?.category === 'GROWING' && - selectedKandang?.status === 'NON_ACTIVE' && + (selectedKandang?.status === 'NON_ACTIVE' && hasActiveProjectWithSameKandang); const { data: projectFlockApprovalResponse } = useSWR( From 75ee058818f452afced51f68243a2bb382430140 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 23 Feb 2026 13:38:47 +0700 Subject: [PATCH 005/149] feat(FE): Add filter functionality to ProjectFlockTable --- .../project-flock/ProjectFlockTable.tsx | 327 ++++++++++++++++-- .../filter/ProjectFlockFilter.ts | 17 + 2 files changed, 323 insertions(+), 21 deletions(-) create mode 100644 src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 7a8ba4e1..7b090c11 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -3,7 +3,10 @@ import Button from '@/components/Button'; import CheckboxInput from '@/components/input/CheckboxInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import { OptionType, useSelect } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; @@ -22,6 +25,7 @@ import { useRouter } from 'next/navigation'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; +import { useFormik } from 'formik'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; @@ -32,6 +36,12 @@ import { useProjectFlockStore } from '@/stores/production/project-flock/project- import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; import { useChickinStore } from '@/stores/production/chickin/chickin.store'; import { useProjectFlockClosingStore } from '@/stores/production/project-flock-closing/project-flock-closing.store'; +import { + ProjectFlockFilterSchema, + ProjectFlockFilterType, +} from './filter/ProjectFlockFilter'; +import Modal from '@/components/Modal'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; const RowOptionsMenu = ({ props, @@ -154,19 +164,19 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { } = useTableFilter({ initial: { search: '', - areaFilter: '', - locationFilter: '', - kandangFilter: '', - periodFilter: '', + area_id: '', + location_id: '', + kandang_id: '', + category: '', }, paramMap: { page: 'page', pageSize: 'limit', search: 'search', - areaFilter: 'area_id', - locationFilter: 'location_id', - kandangFilter: 'kandang_id', - periodFilter: 'period', + area_id: 'area_id', + location_id: 'location_id', + kandang_id: 'kandang_id', + category: 'category', }, }); const router = useRouter(); @@ -207,6 +217,169 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setClosingLoading, } = useProjectFlockClosingStore(); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FILTER DEPENDENCIES STATE ===== + const [filterAreaId, setFilterAreaId] = useState( + undefined + ); + const [filterLocationId, setFilterLocationId] = useState( + undefined + ); + + // ===== FORMIK SETUP FOR FILTER ===== + const formik = useFormik({ + initialValues: { + area_id: null, + location_id: null, + kandang_id: null, + category: null, + }, + validationSchema: ProjectFlockFilterSchema, + onSubmit: (values, { setSubmitting }) => { + updateFilter('area_id', values.area_id || ''); + updateFilter('location_id', values.location_id || ''); + updateFilter('kandang_id', values.kandang_id || ''); + updateFilter('category', values.category || ''); + filterModal.closeModal(); + setSubmitting(false); + }, + onReset: () => { + updateFilter('area_id', ''); + updateFilter('location_id', ''); + updateFilter('kandang_id', ''); + updateFilter('category', ''); + setFilterAreaId(undefined); + setFilterLocationId(undefined); + filterModal.closeModal(); + }, + }); + + // ===== FILTER OPTIONS ===== + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name'); + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + area_id: filterAreaId || '', + }); + + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangOptions, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name', 'search', { + area_id: filterAreaId || '', + location_id: filterLocationId || '', + }); + + const categoryOptions = useMemo( + () => [ + { value: 'GROWING', label: 'Growing' }, + { value: 'LAYING', label: 'Laying' }, + ], + [] + ); + + // ===== FILTER HELPERS ===== + const areaValue = useMemo(() => { + if (!formik.values.area_id) return null; + return ( + areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || + null + ); + }, [formik.values.area_id, areaOptions]); + + const locationValue = useMemo(() => { + if (!formik.values.location_id) return null; + return ( + locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + ); + }, [formik.values.location_id, locationOptions]); + + const kandangValue = useMemo(() => { + if (!formik.values.kandang_id) return null; + return ( + kandangOptions.find( + (opt) => String(opt.value) === formik.values.kandang_id + ) || null + ); + }, [formik.values.kandang_id, kandangOptions]); + + const categoryValue = useMemo(() => { + if (!formik.values.category) return null; + return ( + categoryOptions.find((opt) => opt.value === formik.values.category) || + null + ); + }, [formik.values.category, categoryOptions]); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + if (tableFilterState.area_id) count += 1; + if (tableFilterState.location_id) count += 1; + if (tableFilterState.kandang_id) count += 1; + if (tableFilterState.category) count += 1; + return count; + }, [ + tableFilterState.area_id, + tableFilterState.location_id, + tableFilterState.kandang_id, + tableFilterState.category, + ]); + + const hasFilters = activeFiltersCount > 0; + + // ===== FILTER DEPENDENCY HANDLERS ===== + const handleFilterAreaChange = (area: OptionType | null) => { + const areaId = area?.value ? String(area.value) : undefined; + setFilterAreaId(areaId); + if (!areaId) { + setFilterLocationId(undefined); + formik.setFieldValue('location_id', null); + formik.setFieldValue('kandang_id', null); + } + }; + + const handleFilterLocationChange = (location: OptionType | null) => { + const locationId = location?.value ? String(location.value) : undefined; + setFilterLocationId(locationId); + if (!locationId) { + formik.setFieldValue('kandang_id', null); + } + }; + + // ===== HANDLE FILTER MODAL OPEN ===== + const handleFilterModalOpen = () => { + const areaId = tableFilterState.area_id || null; + const locationId = tableFilterState.location_id || null; + + formik.setValues({ + area_id: areaId, + location_id: locationId, + kandang_id: tableFilterState.kandang_id || null, + category: tableFilterState.category || null, + }); + + setFilterAreaId(areaId || undefined); + setFilterLocationId(locationId || undefined); + + filterModal.openModal(); + }; + // ===== Fetch Data ===== const { data: projectFlocks, @@ -768,26 +941,21 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { void }) => { }} /> + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ { + if (!Array.isArray(val)) { + const areaValue = val?.value ? String(val.value) : null; + formik.setFieldValue('area_id', areaValue); + handleFilterAreaChange(val || null); + } + }} + onInputChange={setAreaInputValue} + isLoading={isLoadingAreaOptions} + isClearable + onMenuScrollToBottom={loadMoreAreas} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + const locationValue = val?.value ? String(val.value) : null; + formik.setFieldValue('location_id', locationValue); + handleFilterLocationChange(val || null); + } + }} + onInputChange={setLocationInputValue} + isLoading={isLoadingLocationOptions} + isClearable + onMenuScrollToBottom={loadMoreLocations} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'kandang_id', + val?.value ? String(val.value) : null + ); + } + }} + onInputChange={setKandangInputValue} + isLoading={isLoadingKandangOptions} + isClearable + onMenuScrollToBottom={loadMoreKandangs} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('category', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> +
+ + {/* Modal Footer */} +
+ + +
+
+
+ {/* Project Flock Closing Modal */} ; From ba28d64562a894d277f620f59e8f3c423e25e1f1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 23 Feb 2026 13:40:00 +0700 Subject: [PATCH 006/149] refactor(FE): Format type definition for readability --- .../production/project-flock/filter/ProjectFlockFilter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts b/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts index 85461c93..a4f4377b 100644 --- a/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts +++ b/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts @@ -14,4 +14,6 @@ export const ProjectFlockFilterSchema = yup.object({ category: yup.string().nullable(), }); -export type ProjectFlockFilterValues = yup.InferType; +export type ProjectFlockFilterValues = yup.InferType< + typeof ProjectFlockFilterSchema +>; From 62c9ab014dbd2a5d2cd98a5e8739aaa51e833ffc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 23 Feb 2026 13:51:32 +0700 Subject: [PATCH 007/149] refactor(FE): Add period filter to ProjectFlockTable --- .../project-flock/ProjectFlockTable.tsx | 37 +++++++++++++++++++ .../filter/ProjectFlockFilter.ts | 2 + 2 files changed, 39 insertions(+) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 7b090c11..dbf1804b 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -168,6 +168,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { location_id: '', kandang_id: '', category: '', + period: '', }, paramMap: { page: 'page', @@ -177,6 +178,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { location_id: 'location_id', kandang_id: 'kandang_id', category: 'category', + period: 'period', }, }); const router = useRouter(); @@ -235,6 +237,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { location_id: null, kandang_id: null, category: null, + period: null, }, validationSchema: ProjectFlockFilterSchema, onSubmit: (values, { setSubmitting }) => { @@ -242,6 +245,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { updateFilter('location_id', values.location_id || ''); updateFilter('kandang_id', values.kandang_id || ''); updateFilter('category', values.category || ''); + updateFilter('period', values.period || ''); filterModal.closeModal(); setSubmitting(false); }, @@ -250,6 +254,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { updateFilter('location_id', ''); updateFilter('kandang_id', ''); updateFilter('category', ''); + updateFilter('period', ''); setFilterAreaId(undefined); setFilterLocationId(undefined); filterModal.closeModal(); @@ -291,6 +296,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { [] ); + const periodOptions = useMemo( + () => [ + { value: '1', label: 'Periode 1' }, + { value: '2', label: 'Periode 2' }, + ], + [] + ); + // ===== FILTER HELPERS ===== const areaValue = useMemo(() => { if (!formik.values.area_id) return null; @@ -326,6 +339,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ); }, [formik.values.category, categoryOptions]); + const periodValue = useMemo(() => { + if (!formik.values.period) return null; + return ( + periodOptions.find((opt) => opt.value === formik.values.period) || null + ); + }, [formik.values.period, periodOptions]); + // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; @@ -333,12 +353,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { if (tableFilterState.location_id) count += 1; if (tableFilterState.kandang_id) count += 1; if (tableFilterState.category) count += 1; + if (tableFilterState.period) count += 1; return count; }, [ tableFilterState.area_id, tableFilterState.location_id, tableFilterState.kandang_id, tableFilterState.category, + tableFilterState.period, ]); const hasFilters = activeFiltersCount > 0; @@ -372,6 +394,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { location_id: locationId, kandang_id: tableFilterState.kandang_id || null, category: tableFilterState.category || null, + period: tableFilterState.period || null, }); setFilterAreaId(areaId || undefined); @@ -1274,6 +1297,20 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { className={{ wrapper: 'w-full' }} isClearable={true} /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('period', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable + /> {/* Modal Footer */} diff --git a/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts b/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts index a4f4377b..a31d6680 100644 --- a/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts +++ b/src/components/pages/production/project-flock/filter/ProjectFlockFilter.ts @@ -5,6 +5,7 @@ export type ProjectFlockFilterType = { location_id: string | null; kandang_id: string | null; category: string | null; + period: string | null; }; export const ProjectFlockFilterSchema = yup.object({ @@ -12,6 +13,7 @@ export const ProjectFlockFilterSchema = yup.object({ location_id: yup.string().nullable(), kandang_id: yup.string().nullable(), category: yup.string().nullable(), + period: yup.string().nullable(), }); export type ProjectFlockFilterValues = yup.InferType< From 81e4e1fc6aa76a0c7926c0be8ddd2502195c3f91 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 23 Feb 2026 14:12:44 +0700 Subject: [PATCH 008/149] refactor(FE): Update validation rules for SalesOrderProduct schema --- .../form/repeater/sales-order/SalesOrderProduct.schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts index 390756e7..8bd5bf72 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts @@ -70,13 +70,13 @@ export const SalesOrderProductSchema: Yup.ObjectSchema Date: Mon, 23 Feb 2026 15:30:54 +0700 Subject: [PATCH 009/149] refactor(FE): Refactor form field change handlers for real-time calculations --- .../DeliverOrderProduct.schema.ts | 16 +++ .../delivery-order/DeliverOrderProduct.tsx | 97 ++++++++++++------- .../sales-order/SalesOrderProduct.schema.ts | 20 +++- .../sales-order/SalesOrderProductForm.tsx | 97 ++++++++++++------- 4 files changed, 158 insertions(+), 72 deletions(-) diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts index c7cb4e9f..bbc6986d 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts @@ -44,13 +44,29 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema + value === '' || value === null ? 0 : Number(value) + ) .min(0, 'Total Bobot wajib diisi!') + .test( + 'is-greater-than-zero', + 'Total Bobot harus lebih dari 0!', + (value) => value !== undefined && value > 0 + ) .required('Total Bobot wajib diisi!'), qty: Yup.number() .min(1, 'Kuantitas wajib diisi!') .required('Kuantitas wajib diisi!'), avg_weight: Yup.number() + .transform((value) => + value === '' || value === null ? 0 : Number(value) + ) .min(0, 'Avg. Bobot wajib diisi!') + .test( + 'is-greater-than-zero', + 'Avg. Bobot harus lebih dari 0!', + (value) => value !== undefined && value > 0 + ) .required('Avg. Bobot wajib diisi!'), total_price: Yup.number() .min(1, 'Total Penjualan wajib diisi!') diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index aae37d8e..c10976eb 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -236,6 +236,25 @@ const DeliveryOrderProductForm = ({ }); }; + // Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty) + const handleFieldChange = ( + field: string, + value: number | string, + callback?: () => void + ) => { + formik.setFieldValue(field, value); + + setTimeout(() => { + handleMarketingCalculation(field, { + values: { ...formik.values, [field]: value }, + setFieldValue: formik.setFieldValue, + hasSisaBerat, + }); + }, 0); + + if (callback) callback(); + }; + // Handler khusus untuk toggle sisa berat - langsung pakai nilai baru const handleSisaBeratToggle = (newHasSisaBerat: boolean) => { setHasSisaBerat(newHasSisaBerat); @@ -520,13 +539,11 @@ const DeliveryOrderProductForm = ({ } per ${formik.values.convertion_unit?.value}`} value={formik.values.weight_per_convertion ?? ''} onChange={(e) => { - formik.setFieldValue( - 'weight_per_convertion', - Number(e.target.value) + const value = Number(e.target.value); + handleFieldChange('weight_per_convertion', value, () => + setCurrentInput(e.target.name) ); - setCurrentInput(e.target.name); }} - onBlur={() => handleBlurField('weight_per_convertion')} /> @@ -564,10 +581,11 @@ const DeliveryOrderProductForm = ({ name='total_peti' value={formik.values.total_peti ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_peti', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('total_peti')} isError={ formik.touched.total_peti && Boolean(formik.errors.total_peti) } @@ -592,10 +610,11 @@ const DeliveryOrderProductForm = ({ name='avg_weight' value={formik.values.avg_weight} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('avg_weight', value, () => + setCurrentInput('avg_weight') + ); }} - onBlur={() => handleBlurField('avg_weight')} isError={ formik.touched.avg_weight && Boolean(formik.errors.avg_weight) @@ -613,10 +632,11 @@ const DeliveryOrderProductForm = ({ name='total_weight' value={formik.values.total_weight} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_weight', value, () => + setCurrentInput('total_weight') + ); }} - onBlur={() => handleBlurField('total_weight')} isError={ formik.touched.total_weight && Boolean(formik.errors.total_weight) @@ -638,10 +658,11 @@ const DeliveryOrderProductForm = ({ name='qty' value={formik.values.qty} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('qty', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('qty')} isError={Boolean(formik.errors.qty)} errorMessage={formik.errors.qty} placeholder='Masukan Kuantitas' @@ -677,10 +698,11 @@ const DeliveryOrderProductForm = ({ name='price_per_convertion' value={formik.values.price_per_convertion ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('price_per_convertion', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('price_per_convertion')} isError={ formik.touched.price_per_convertion && Boolean(formik.errors.price_per_convertion) @@ -699,10 +721,11 @@ const DeliveryOrderProductForm = ({ name='price_per_qty' value={formik.values.price_per_qty ?? undefined} onChange={(e) => { - formik.setFieldValue('price_per_qty', Number(e.target.value)); - setCurrentInput('price_per_qty'); + const value = Number(e.target.value); + handleFieldChange('price_per_qty', value, () => + setCurrentInput('price_per_qty') + ); }} - onBlur={() => handleBlurField('price_per_qty')} isError={ formik.touched.price_per_qty && Boolean(formik.errors.price_per_qty) @@ -721,10 +744,11 @@ const DeliveryOrderProductForm = ({ name='unit_price' value={formik.values.unit_price} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('unit_price', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('unit_price')} isError={Boolean(formik.errors.unit_price)} errorMessage={formik.errors.unit_price} placeholder='Masukan Harga Satuan' @@ -760,10 +784,11 @@ const DeliveryOrderProductForm = ({ name='sisa_berat' value={formik.values.sisa_berat ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('sisa_berat', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('sisa_berat')} isError={ formik.touched.sisa_berat && Boolean(formik.errors.sisa_berat) } @@ -776,10 +801,11 @@ const DeliveryOrderProductForm = ({ name='price_sisa_berat' value={formik.values.price_sisa_berat ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('price_sisa_berat', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('price_sisa_berat')} isError={ formik.touched.price_sisa_berat && Boolean(formik.errors.price_sisa_berat) @@ -797,10 +823,11 @@ const DeliveryOrderProductForm = ({ name='total_price' value={formik.values.total_price} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_price', value, () => + setCurrentInput('total_price') + ); }} - onBlur={() => handleBlurField('total_price')} isError={ formik.touched.total_price && Boolean(formik.errors.total_price) } diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts index 8bd5bf72..349e0d77 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts @@ -70,13 +70,29 @@ export const SalesOrderProductSchema: Yup.ObjectSchema + value === '' || value === null ? 0 : Number(value) + ) + .min(0, 'Total Bobot tidak boleh negatif!') + .test( + 'is-greater-than-zero', + 'Total Bobot harus lebih dari 0!', + (value) => value !== undefined && value > 0 + ) .required('Total Bobot wajib diisi!'), qty: Yup.number() .min(1, 'Kuantitas wajib diisi!') .required('Kuantitas wajib diisi!'), avg_weight: Yup.number() - .min(1, 'Avg. Bobot wajib diisi!') + .transform((value) => + value === '' || value === null ? 0 : Number(value) + ) + .min(0, 'Avg. Bobot wajib diisi!') + .test( + 'is-greater-than-zero', + 'Avg. Bobot harus lebih dari 0!', + (value) => value !== undefined && value > 0 + ) .required('Avg. Bobot wajib diisi!'), total_price: Yup.number() .min(1, 'Total Penjualan wajib diisi!') diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index 70965071..8da873e5 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -250,6 +250,25 @@ const SalesOrderProductForm = ({ }); }; + // Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty) + const handleFieldChange = ( + field: string, + value: number | string, + callback?: () => void + ) => { + formik.setFieldValue(field, value); + + setTimeout(() => { + handleMarketingCalculation(field, { + values: { ...formik.values, [field]: value }, + setFieldValue: formik.setFieldValue, + hasSisaBerat, + }); + }, 0); + + if (callback) callback(); + }; + // Handler khusus untuk toggle sisa berat - langsung pakai nilai baru const handleSisaBeratToggle = (newHasSisaBerat: boolean) => { setHasSisaBerat(newHasSisaBerat); @@ -475,13 +494,11 @@ const SalesOrderProductForm = ({ } per ${formik.values.convertion_unit?.value}`} value={formik.values.weight_per_convertion ?? ''} onChange={(e) => { - formik.setFieldValue( - 'weight_per_convertion', - Number(e.target.value) + const value = Number(e.target.value); + handleFieldChange('weight_per_convertion', value, () => + setCurrentInput(e.target.name) ); - setCurrentInput(e.target.name); }} - onBlur={() => handleBlurField('weight_per_convertion')} /> @@ -519,10 +536,11 @@ const SalesOrderProductForm = ({ name='total_peti' value={formik.values.total_peti ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_peti', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('total_peti')} isError={ formik.touched.total_peti && Boolean(formik.errors.total_peti) } @@ -547,10 +565,11 @@ const SalesOrderProductForm = ({ name='avg_weight' value={formik.values.avg_weight} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('avg_weight', value, () => + setCurrentInput('avg_weight') + ); }} - onBlur={() => handleBlurField('avg_weight')} isError={ formik.touched.avg_weight && Boolean(formik.errors.avg_weight) @@ -568,10 +587,11 @@ const SalesOrderProductForm = ({ name='total_weight' value={formik.values.total_weight} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_weight', value, () => + setCurrentInput('total_weight') + ); }} - onBlur={() => handleBlurField('total_weight')} isError={ formik.touched.total_weight && Boolean(formik.errors.total_weight) @@ -593,10 +613,11 @@ const SalesOrderProductForm = ({ name='qty' value={formik.values.qty} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('qty', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('qty')} isError={formik.touched.qty && Boolean(formik.errors.qty)} errorMessage={formik.errors.qty} placeholder='Masukan Kuantitas' @@ -630,10 +651,11 @@ const SalesOrderProductForm = ({ name='price_per_convertion' value={formik.values.price_per_convertion ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('price_per_convertion', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('price_per_convertion')} isError={ formik.touched.price_per_convertion && Boolean(formik.errors.price_per_convertion) @@ -652,10 +674,11 @@ const SalesOrderProductForm = ({ name='price_per_qty' value={formik.values.price_per_qty ?? undefined} onChange={(e) => { - formik.setFieldValue('price_per_qty', Number(e.target.value)); - setCurrentInput('price_per_qty'); + const value = Number(e.target.value); + handleFieldChange('price_per_qty', value, () => + setCurrentInput('price_per_qty') + ); }} - onBlur={() => handleBlurField('price_per_qty')} isError={ formik.touched.price_per_qty && Boolean(formik.errors.price_per_qty) @@ -674,10 +697,11 @@ const SalesOrderProductForm = ({ name='unit_price' value={formik.values.unit_price} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('unit_price', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('unit_price')} isError={ formik.touched.unit_price && Boolean(formik.errors.unit_price) } @@ -715,10 +739,11 @@ const SalesOrderProductForm = ({ name='sisa_berat' value={formik.values.sisa_berat ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('sisa_berat', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('sisa_berat')} isError={ formik.touched.sisa_berat && Boolean(formik.errors.sisa_berat) } @@ -731,10 +756,11 @@ const SalesOrderProductForm = ({ name='price_sisa_berat' value={formik.values.price_sisa_berat ?? undefined} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('price_sisa_berat', value, () => + setCurrentInput(e.target.name) + ); }} - onBlur={() => handleBlurField('price_sisa_berat')} isError={ formik.touched.price_sisa_berat && Boolean(formik.errors.price_sisa_berat) @@ -752,10 +778,11 @@ const SalesOrderProductForm = ({ name='total_price' value={formik.values.total_price} onChange={(e) => { - formik.handleChange(e); - setCurrentInput(e.target.name); + const value = Number(e.target.value); + handleFieldChange('total_price', value, () => + setCurrentInput('total_price') + ); }} - onBlur={() => handleBlurField('total_price')} isError={ formik.touched.total_price && Boolean(formik.errors.total_price) } From 50424a25fc19a221539d1387115f09eb1cf8329e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 23 Feb 2026 16:05:12 +0700 Subject: [PATCH 010/149] refactor(FE): Remove redundant transformations and validations in schemas --- .../DeliverOrderProduct.schema.ts | 16 ---------------- .../sales-order/SalesOrderProduct.schema.ts | 18 +----------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts index bbc6986d..c7cb4e9f 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts @@ -44,29 +44,13 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema - value === '' || value === null ? 0 : Number(value) - ) .min(0, 'Total Bobot wajib diisi!') - .test( - 'is-greater-than-zero', - 'Total Bobot harus lebih dari 0!', - (value) => value !== undefined && value > 0 - ) .required('Total Bobot wajib diisi!'), qty: Yup.number() .min(1, 'Kuantitas wajib diisi!') .required('Kuantitas wajib diisi!'), avg_weight: Yup.number() - .transform((value) => - value === '' || value === null ? 0 : Number(value) - ) .min(0, 'Avg. Bobot wajib diisi!') - .test( - 'is-greater-than-zero', - 'Avg. Bobot harus lebih dari 0!', - (value) => value !== undefined && value > 0 - ) .required('Avg. Bobot wajib diisi!'), total_price: Yup.number() .min(1, 'Total Penjualan wajib diisi!') diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts index 349e0d77..390756e7 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts @@ -70,29 +70,13 @@ export const SalesOrderProductSchema: Yup.ObjectSchema - value === '' || value === null ? 0 : Number(value) - ) - .min(0, 'Total Bobot tidak boleh negatif!') - .test( - 'is-greater-than-zero', - 'Total Bobot harus lebih dari 0!', - (value) => value !== undefined && value > 0 - ) + .min(0, 'Total Bobot wajib diisi!') .required('Total Bobot wajib diisi!'), qty: Yup.number() .min(1, 'Kuantitas wajib diisi!') .required('Kuantitas wajib diisi!'), avg_weight: Yup.number() - .transform((value) => - value === '' || value === null ? 0 : Number(value) - ) .min(0, 'Avg. Bobot wajib diisi!') - .test( - 'is-greater-than-zero', - 'Avg. Bobot harus lebih dari 0!', - (value) => value !== undefined && value > 0 - ) .required('Avg. Bobot wajib diisi!'), total_price: Yup.number() .min(1, 'Total Penjualan wajib diisi!') From 509fc5476d78da35e05e47ac392bf519055820b3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 24 Feb 2026 14:48:09 +0700 Subject: [PATCH 011/149] refactor(FE): Refactor recording table and add filter modal --- src/app/production/recording/page.tsx | 2 +- .../production/recording/RecordingTable.tsx | 1852 +++++++++++------ .../recording/filter/RecordingFilter.ts | 15 + .../skeleton/RecordingTableSkeleton.tsx | 37 + 4 files changed, 1217 insertions(+), 689 deletions(-) create mode 100644 src/components/pages/production/recording/filter/RecordingFilter.ts create mode 100644 src/components/pages/production/recording/skeleton/RecordingTableSkeleton.tsx diff --git a/src/app/production/recording/page.tsx b/src/app/production/recording/page.tsx index 471ef648..fbbac7cb 100644 --- a/src/app/production/recording/page.tsx +++ b/src/app/production/recording/page.tsx @@ -2,7 +2,7 @@ import RecordingTable from '@/components/pages/production/recording/RecordingTab const Recording = () => { return ( -
+
); diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 65e658f9..bd26670d 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -9,21 +9,33 @@ import React, { } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; -import { SortingState, CellContext } from '@tanstack/react-table'; +import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table'; import { cn, formatDate, formatNumber } from '@/lib/helper'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useModal } from '@/components/Modal'; +import Modal, { useModal } from '@/components/Modal'; import Button from '@/components/Button'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import { OptionType } from '@/components/input/SelectInput'; -import SelectInput from '@/components/input/SelectInput'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import { useFormik } from 'formik'; +import { AreaApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; +import { ProjectFlockApi } from '@/services/api/production'; +import { Location } from '@/types/api/master-data/location'; +import { Area } from '@/types/api/master-data/area'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { type BaseApiResponse } from '@/types/api/api-general'; +import { + RecordingFilterSchema, + RecordingFilterType, +} from '@/components/pages/production/recording/filter/RecordingFilter'; +import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton'; import { ROWS_OPTIONS } from '@/config/constant'; import Table from '@/components/Table'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import { type Recording } from '@/types/api/production/recording'; import { RecordingApi } from '@/services/api/production'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -61,18 +73,25 @@ const getStatusBadgeColor = (status: string): Color => { }; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, approveClickHandler, rejectClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; approveClickHandler: () => void; rejectClickHandler: () => void; }) => { + const popoverId = `recording#${props.row.original.id}`; + const popoverAnchorName = `--anchor-recording#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + const isRecordingApproved = (recording: Recording) => { return ( recording.approval?.action === 'APPROVED' && @@ -89,72 +108,97 @@ const RowOptionsMenu = ({ const isRejected = isRecordingRejected(props.row.original); return ( - - - - - - - - {!isApproved && !isRejected && ( - - - - )} - {!isApproved && !isRejected && ( - - - - )} - - - - +
+ + + + + +
+ + + + + + + {!isApproved && !isRejected && ( + + + + )} + {!isApproved && !isRejected && ( + + + + )} + + + +
+
+
); }; @@ -174,16 +218,58 @@ const RecordingTable = () => { areaFilter: '', locationFilter: '', kandangFilter: '', - periodFilter: '', + projectFlockKandangFilter: '', }, paramMap: { page: 'page', pageSize: 'limit', search: 'search', - areaFilter: 'area_id', - locationFilter: 'location_id', kandangFilter: 'kandang_id', - periodFilter: 'period', + projectFlockKandangFilter: 'project_flock_kandang_id', + }, + }); + + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FILTER STATE ===== + const [filterArea, setFilterArea] = useState(null); + const [filterLocation, setFilterLocation] = useState(null); + const [filterProjectFlock, setFilterProjectFlock] = + useState(null); + const [filterKandang, setFilterKandang] = useState(null); + const [, setFilterProjectFlockKandangId] = useState( + undefined + ); + const [filterLocationAreaId, setFilterLocationAreaId] = useState(''); + const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] = + useState(''); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + area_id: null, + location_id: null, + kandang_id: null, + project_flock_kandang_id: null, + }, + validationSchema: RecordingFilterSchema, + onSubmit: (values, { setSubmitting }) => { + updateFilter('areaFilter', values.area_id || ''); + updateFilter('locationFilter', values.location_id || ''); + updateFilter('kandangFilter', values.kandang_id || ''); + updateFilter( + 'projectFlockKandangFilter', + values.project_flock_kandang_id || '' + ); + filterModal.closeModal(); + setSubmitting(false); + }, + onReset: () => { + updateFilter('areaFilter', ''); + updateFilter('locationFilter', ''); + updateFilter('kandangFilter', ''); + updateFilter('projectFlockKandangFilter', ''); }, }); @@ -213,6 +299,250 @@ const RecordingTable = () => { RecordingApi.getAllFetcher ); + // ===== LOCATION, AREA, KANDANG OPTIONS ===== + const locationParams = useMemo(() => { + if (filterLocationAreaId) { + return { area_id: filterLocationAreaId }; + } + return undefined; + }, [filterLocationAreaId]); + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect( + LocationApi.basePath, + 'id', + 'name', + 'search', + locationParams + ); + + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + + const projectFlockParams = useMemo(() => { + if (filterProjectFlockLocationId) { + return { location_id: filterProjectFlockLocationId }; + } + return undefined; + }, [filterProjectFlockLocationId]); + + const { + setInputValue: setProjectFlockInputValue, + options: projectFlockOptions, + rawData: projectFlocksRawData, + isLoadingOptions: isLoadingProjectFlocks, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + projectFlockParams + ); + + const kandangOptions = useMemo(() => { + if (!filterProjectFlock || !projectFlocksRawData) return []; + if (!isResponseSuccess(projectFlocksRawData)) return []; + + const data = projectFlocksRawData.data as ProjectFlock[]; + const selectedProjectFlockData = data.find( + (pf) => pf.id === filterProjectFlock.value + ); + + if (!selectedProjectFlockData?.kandangs) return []; + return selectedProjectFlockData.kandangs.map((k) => ({ + value: k.id, + label: k.name || '', + })); + }, [filterProjectFlock, projectFlocksRawData]); + + // ===== PROJECT FLOCK KANDANG LOOKUP ===== + const projectFlockKandangLookupUrl = useMemo(() => { + if (!filterProjectFlock || !filterKandang) return null; + const params = new URLSearchParams({ + project_flock_id: filterProjectFlock.value.toString(), + kandang_id: filterKandang.value.toString(), + }); + return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; + }, [filterProjectFlock, filterKandang]); + + const { data: projectFlockKandangLookupData } = useSWR( + projectFlockKandangLookupUrl, + projectFlockKandangLookupUrl + ? () => + ProjectFlockApi.getAllFetcher( + projectFlockKandangLookupUrl + ) as Promise< + BaseApiResponse<{ + id: number; + project_flock: { flock_name: string }; + }> + > + : null + ); + + const projectFlockKandangLookup = + projectFlockKandangLookupData?.status === 'success' + ? projectFlockKandangLookupData.data + : undefined; + + const formikRef = useRef(formik); + + useEffect(() => { + formikRef.current = formik; + }); + + useEffect(() => { + if (projectFlockKandangLookup?.id) { + const pfkId = String(projectFlockKandangLookup.id); + setFilterProjectFlockKandangId(projectFlockKandangLookup.id); + formikRef.current.setFieldValue('project_flock_kandang_id', pfkId); + } else { + setFilterProjectFlockKandangId(undefined); + formikRef.current.setFieldValue('project_flock_kandang_id', null); + } + }, [projectFlockKandangLookup]); + + // ===== FILTER HANDLERS ===== + const handleFilterAreaChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const area = val as OptionType | null; + const areaId = area?.value ? String(area.value) : null; + + formik.setFieldValue('area_id', areaId); + formik.setFieldValue('location_id', null); + formik.setFieldValue('kandang_id', null); + formik.setFieldValue('project_flock_kandang_id', null); + + setFilterArea(area); + setFilterLocation(null); + setFilterProjectFlock(null); + setFilterKandang(null); + setFilterLocationAreaId(areaId || ''); + setFilterProjectFlockLocationId(''); + }, + [formik] + ); + + const handleFilterLocationChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const location = val as OptionType | null; + const locationId = location?.value ? String(location.value) : null; + + formik.setFieldValue('location_id', locationId); + formik.setFieldValue('kandang_id', null); + formik.setFieldValue('project_flock_kandang_id', null); + + setFilterLocation(location); + setFilterProjectFlock(null); + setFilterKandang(null); + setFilterProjectFlockLocationId(locationId || ''); + }, + [formik] + ); + + const handleFilterProjectFlockChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const projectFlock = val as OptionType | null; + + formik.setFieldValue('kandang_id', null); + formik.setFieldValue('project_flock_kandang_id', null); + + setFilterProjectFlock(projectFlock); + setFilterKandang(null); + }, + [formik] + ); + + const handleFilterKandangChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const kandang = val as OptionType | null; + const kandangId = kandang?.value ? String(kandang.value) : null; + + formik.setFieldValue('kandang_id', kandangId); + formik.setFieldValue('project_flock_kandang_id', null); + + setFilterKandang(kandang); + }, + [formik] + ); + + // ===== FILTER HELPERS ===== + const areaIdValue = useMemo(() => { + if (!formik.values.area_id) return null; + return ( + areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || + null + ); + }, [formik.values.area_id, areaOptions]); + + const locationIdValue = useMemo(() => { + if (!formik.values.location_id) return null; + return ( + locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + ); + }, [formik.values.location_id, locationOptions]); + + const projectFlockIdValue = useMemo(() => { + if (!filterProjectFlock) return null; + return filterProjectFlock; + }, [filterProjectFlock]); + + const kandangIdValue = useMemo(() => { + if (!formik.values.kandang_id) return null; + return ( + kandangOptions.find( + (opt) => String(opt.value) === formik.values.kandang_id + ) || null + ); + }, [formik.values.kandang_id, kandangOptions]); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (tableFilterState.areaFilter) { + count += 1; + } + + if (tableFilterState.locationFilter) { + count += 1; + } + + if (tableFilterState.kandangFilter) { + count += 1; + } + + if (tableFilterState.projectFlockKandangFilter) { + count += 1; + } + + return count; + }, [ + tableFilterState.areaFilter, + tableFilterState.locationFilter, + tableFilterState.kandangFilter, + tableFilterState.projectFlockKandangFilter, + ]); + + const hasFilters = activeFiltersCount > 0; + + // ===== HANDLE FILTER MODAL OPEN ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + const isRecordingApproved = useCallback((recording: Recording): boolean => { return ( recording.approval?.action === 'APPROVED' && @@ -221,13 +551,11 @@ const RecordingTable = () => { }, []); useEffect(() => { - // Store current path on mount previousPathRef.current = window.location.pathname; return () => { const currentPath = window.location.pathname; - // if both paths are within /production/recording module const isCurrentPathRecording = currentPath.includes( '/production/recording' ); @@ -235,7 +563,6 @@ const RecordingTable = () => { '/production/recording' ); - // reset if we outside recording module entirely if (isPreviousPathRecording && !isCurrentPathRecording) { resetSearchValue(); } @@ -360,624 +687,773 @@ const RecordingTable = () => { } }, [recordings, rowSelection, isRecordingApproved, setRowSelection]); + // ===== TABLE COLUMNS ===== + const recordingColumns: ColumnDef[] = useMemo( + () => [ + { + id: 'select', + header: ({ table }) => { + const allRows = table.getRowModel().rows; + const selectableRows = allRows.filter((row) => { + const recording = row.original; + return !isRecordingApproved(recording); + }); + + const hasNoSelectableRows = selectableRows.length === 0; + + const handleSelectAll = () => { + const isAllSelected = selectableRows.every((row) => + row.getIsSelected() + ); + + selectableRows.forEach((row) => { + row.toggleSelected(!isAllSelected); + }); + }; + + const isAllSelected = + selectableRows.length > 0 && + selectableRows.every((row) => row.getIsSelected()); + + const isSomeSelected = selectableRows.some((row) => + row.getIsSelected() + ); + + return ( +
+ +
+ ); + }, + cell: ({ row }) => { + const recording = row.original; + const isDisabled = isRecordingApproved(recording); + + const handleToggleSelection = (e: unknown) => { + if (!isDisabled) { + row.getToggleSelectedHandler()(e); + } + }; + + return ( +
+ +
+ ); + }, + }, + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + header: 'Lokasi', + cell: (props) => props.row.original.location?.name || '-', + }, + { + header: 'Flock', + cell: (props) => props.row.original.project_flock?.flock_name || '-', + }, + { + header: 'Kandang', + cell: (props) => props.row.original.kandang?.name || '-', + }, + { + header: 'Periode', + cell: (props) => props.row.original.project_flock?.period || '-', + }, + { + header: 'Kategori', + cell: (props) => { + const category = + props.row.original.project_flock?.project_flock_category; + if (!category) return '-'; + const color = category === 'LAYING' ? 'info' : 'warning'; + return ( + + {category} + + ); + }, + }, + { + header: 'Umur (hari)', + cell: (props) => { + return ( + <> + + {props.row.original.day} (Minggu ke- + {props.row.original.project_flock.production_standart.week}) + + + ); + }, + }, + { + header: 'Waktu Recording', + cell: (props) => + formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'), + }, + { + header: 'Populasi Akhir', + cell: (props) => + props.row.original.project_flock?.total_chick_qty != null + ? formatNumber(props.row.original.project_flock.total_chick_qty) + : '-', + }, + { + id: 'fcr', + header: 'FCR', + columns: [ + { + id: 'fcr_actual', + header: 'Actual', + cell: (props) => { + const value = props.row.original.fcr_value; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'fcr_standard', + header: 'Standard', + cell: (props) => { + const value = props.row.original.project_flock?.fcr?.fcr_std; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + ], + }, + { + id: 'feed_intake', + header: 'Feed Intake (KG)', + columns: [ + { + id: 'feed_intake_actual', + header: 'Actual', + cell: (props) => { + const value = props.row.original.feed_intake; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'feed_intake_standard', + header: 'Standard', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.feed_intake_std; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + ], + }, + { + id: 'mortality', + header: 'Mortality', + columns: [ + { + id: 'cum_depletion_rate_actual', + header: 'Cum Depletion Rate', + cell: (props) => { + const value = props.row.original.cum_depletion_rate; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'max_depletion_std', + header: 'Max Depletion Std', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.max_depletion_std; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'total_depletion', + header: 'Total Depletion', + cell: (props) => { + const value = props.row.original.total_depletion_qty; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + ], + }, + { + id: 'egg_production', + header: 'Egg Production', + columns: [ + { + id: 'egg_mass_actual', + header: 'Egg Mass Actual', + cell: (props) => { + const value = props.row.original.egg_mass; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'egg_mass_standard', + header: 'Egg Mass Standar', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.egg_mass_std; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'egg_weight_actual', + header: 'Egg Weight Actual', + cell: (props) => { + const value = props.row.original.egg_weight; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + { + id: 'egg_weight_standard', + header: 'Egg Weight Standar', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.egg_weight_std; + return ( +
+ {value !== null && value !== undefined + ? formatNumber(value) + : '-'} +
+ ); + }, + }, + ], + }, + { + id: 'hen_performance', + header: 'Hen Performance', + columns: [ + { + id: 'hen_day_actual', + header: 'Hen Day Actual', + cell: (props) => { + const value = props.row.original.hen_day; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'hen_day_standard', + header: 'Hen Day Standar', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.hen_day_std; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'hen_house_actual', + header: 'Hen House Actual', + cell: (props) => { + const value = props.row.original.hen_house; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + { + id: 'hen_house_standard', + header: 'Hen House Standar', + cell: (props) => { + const value = + props.row.original.project_flock?.production_standart + ?.hen_house_std; + return ( +
+ {value !== null && value !== undefined + ? `${value.toFixed(2)}%` + : '-'} +
+ ); + }, + }, + ], + }, + { + header: 'Status Approval', + cell: (props) => { + const approval = props.row.original.approval; + if (!approval) return '-'; + + const status = approval.action; + const statusColor = getStatusBadgeColor(status); + const statusText = getStatusText(status); + + return ( + + ); + }, + }, + { + header: 'Catatan Approval', + cell: (props) => { + const approval = props.row.original.approval; + if (!approval?.notes) return '-'; + + return ( +
+

{approval.notes}

+
+ ); + }, + }, + { + header: 'Dibuat Oleh', + cell: (props) => props.row.original.created_user?.name || '-', + }, + { + header: 'Tanggal Submit', + cell: (props) => + formatDate(props.row.original.created_at, 'DD MMMM YYYY'), + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + const currentPageSize = + props.table.getPaginationRowModel().rows.length; + const currentPageRows = props.table.getPaginationRowModel().flatRows; + const currentRowRelativeIndex = + currentPageRows.findIndex((r) => r.id === props.row.id) + 1; + + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + const deleteClickHandler = () => { + setSelectedRecording(props.row.original); + singleDeleteModal.openModal(); + }; + + const approveClickHandler = () => { + setRowSelection({ + [String(props.row.original.id)]: true, + }); + setApprovalNotes(''); + approveModal.openModal(); + }; + + const rejectClickHandler = () => { + setRowSelection({ + [String(props.row.original.id)]: true, + }); + setApprovalNotes(''); + rejectModal.openModal(); + }; + + return ( + + ); + }, + }, + ], + [ + isRecordingApproved, + tableFilterState.pageSize, + tableFilterState.page, + selectedRecording, + singleDeleteModal, + approveModal, + rejectModal, + rowSelection, + setRowSelection, + setApprovalNotes, + setSelectedRecording, + ] + ); + return ( -
-
-
-
- + <> +
+
+ {/* Action Buttons, Search and Filter Section */} +
+ {/* Action Buttons */} +
+ + + + + {selectedRowIds.length > 0 && ( + <> + + + + + + + + + )} +
+ + {/* Search and Filter */} +
+ + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> + - - {selectedRowIds.length > 0 && ( - <> - - - - - - - - - )} + +
- -
- -
- + {/* Table Section */} + {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(recordings) || + recordings.data?.length === 0 ? ( + + } + title='Data Recording Belum Tersedia' + subtitle='Tidak ada data recording untuk saat ini.' + /> + ) : ( + + data={isResponseSuccess(recordings) ? recordings?.data : []} + columns={recordingColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(recordings) ? recordings?.meta?.page : 0} + totalItems={ + isResponseSuccess(recordings) + ? recordings?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn('mt-3 mb-0', { + 'w-full': + isResponseSuccess(recordings) && + recordings?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> + )}
- - data={isResponseSuccess(recordings) ? recordings?.data : []} - columns={[ - { - id: 'select', - header: ({ table }) => { - const allRows = table.getRowModel().rows; - const selectableRows = allRows.filter((row) => { - const recording = row.original; - return !isRecordingApproved(recording); - }); - - const hasNoSelectableRows = selectableRows.length === 0; - - const handleSelectAll = () => { - const isAllSelected = selectableRows.every((row) => - row.getIsSelected() - ); - - selectableRows.forEach((row) => { - row.toggleSelected(!isAllSelected); - }); - }; - - const isAllSelected = - selectableRows.length > 0 && - selectableRows.every((row) => row.getIsSelected()); - - const isSomeSelected = selectableRows.some((row) => - row.getIsSelected() - ); - - return ( -
- -
- ); - }, - cell: ({ row }) => { - const recording = row.original; - const isDisabled = isRecordingApproved(recording); - - const handleToggleSelection = (e: unknown) => { - if (!isDisabled) { - row.getToggleSelectedHandler()(e); - } - }; - - return ( -
- -
- ); - }, - }, - { - header: 'No', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - header: 'Lokasi', - cell: (props) => props.row.original.location?.name || '-', - }, - { - header: 'Flock', - cell: (props) => - props.row.original.project_flock?.flock_name || '-', - }, - { - header: 'Kandang', - cell: (props) => props.row.original.kandang?.name || '-', - }, - { - header: 'Periode', - cell: (props) => props.row.original.project_flock?.period || '-', - }, - { - header: 'Kategori', - cell: (props) => { - const category = - props.row.original.project_flock?.project_flock_category; - if (!category) return '-'; - const color = category === 'LAYING' ? 'info' : 'warning'; - return ( - - {category} - - ); - }, - }, - { - header: 'Umur (hari)', - cell: (props) => { - return ( - <> - - {props.row.original.day} (Minggu ke- - {props.row.original.project_flock.production_standart.week}) - - - ); - }, - }, - { - header: 'Waktu Recording', - cell: (props) => - formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'), - }, - { - header: 'Populasi Akhir', - cell: (props) => - props.row.original.project_flock?.total_chick_qty != null - ? formatNumber(props.row.original.project_flock.total_chick_qty) - : '-', - }, - { - id: 'fcr', - header: 'FCR', - columns: [ - { - id: 'fcr_actual', - header: 'Actual', - cell: (props) => { - const value = props.row.original.fcr_value; - return ( -
- {value !== null && value !== undefined - ? formatNumber(value) - : '-'} -
- ); - }, - }, - { - id: 'fcr_standard', - header: 'Standard', - cell: (props) => { - const value = props.row.original.project_flock?.fcr?.fcr_std; - return ( -
- {value !== null && value !== undefined - ? formatNumber(value) - : '-'} -
- ); - }, - }, - ], - }, - { - id: 'feed_intake', - header: 'Feed Intake (KG)', - columns: [ - { - id: 'feed_intake_actual', - header: 'Actual', - cell: (props) => { - const value = props.row.original.feed_intake; - return ( -
- {value !== null && value !== undefined - ? formatNumber(value) - : '-'} -
- ); - }, - }, - { - id: 'feed_intake_standard', - header: 'Standard', - cell: (props) => { - const value = - props.row.original.project_flock?.production_standart - ?.feed_intake_std; - return ( -
- {value !== null && value !== undefined - ? formatNumber(value) - : '-'} -
- ); - }, - }, - ], - }, - { - id: 'mortality', - header: 'Mortality', - columns: [ - { - id: 'cum_depletion_rate_actual', - header: 'Cum Depletion Rate', - cell: (props) => { - const value = props.row.original.cum_depletion_rate; - return ( -
- {value !== null && value !== undefined - ? `${value.toFixed(2)}%` - : '-'} -
- ); - }, - }, - { - id: 'max_depletion_std', - header: 'Max Depletion Std', - cell: (props) => { - const value = - props.row.original.project_flock?.production_standart - ?.max_depletion_std; - return ( -
- {value !== null && value !== undefined - ? `${value.toFixed(2)}%` - : '-'} -
- ); - }, - }, - { - id: 'total_depletion', - header: 'Total Depletion', - cell: (props) => { - const value = props.row.original.total_depletion_qty; - return ( -
- {value !== null && value !== undefined - ? formatNumber(value) - : '-'} -
- ); - }, - }, - ], - }, - { - id: 'egg_production', - header: 'Egg Production', - columns: [ - { - id: 'egg_mass_actual', - header: 'Egg Mass Actual', - cell: (props) => { - const value = props.row.original.egg_mass; - return ( -
- {value !== null && value !== undefined - ? formatNumber(value) - : '-'} -
- ); - }, - }, - { - id: 'egg_mass_standard', - header: 'Egg Mass Standar', - cell: (props) => { - const value = - props.row.original.project_flock?.production_standart - ?.egg_mass_std; - return ( -
- {value !== null && value !== undefined - ? formatNumber(value) - : '-'} -
- ); - }, - }, - { - id: 'egg_weight_actual', - header: 'Egg Weight Actual', - cell: (props) => { - const value = props.row.original.egg_weight; - return ( -
- {value !== null && value !== undefined - ? formatNumber(value) - : '-'} -
- ); - }, - }, - { - id: 'egg_weight_standard', - header: 'Egg Weight Standar', - cell: (props) => { - const value = - props.row.original.project_flock?.production_standart - ?.egg_weight_std; - return ( -
- {value !== null && value !== undefined - ? formatNumber(value) - : '-'} -
- ); - }, - }, - ], - }, - { - id: 'hen_performance', - header: 'Hen Performance', - columns: [ - { - id: 'hen_day_actual', - header: 'Hen Day Actual', - cell: (props) => { - const value = props.row.original.hen_day; - return ( -
- {value !== null && value !== undefined - ? `${value.toFixed(2)}%` - : '-'} -
- ); - }, - }, - { - id: 'hen_day_standard', - header: 'Hen Day Standar', - cell: (props) => { - const value = - props.row.original.project_flock?.production_standart - ?.hen_day_std; - return ( -
- {value !== null && value !== undefined - ? `${value.toFixed(2)}%` - : '-'} -
- ); - }, - }, - { - id: 'hen_house_actual', - header: 'Hen House Actual', - cell: (props) => { - const value = props.row.original.hen_house; - return ( -
- {value !== null && value !== undefined - ? `${value.toFixed(2)}%` - : '-'} -
- ); - }, - }, - { - id: 'hen_house_standard', - header: 'Hen House Standar', - cell: (props) => { - const value = - props.row.original.project_flock?.production_standart - ?.hen_house_std; - return ( -
- {value !== null && value !== undefined - ? `${value.toFixed(2)}%` - : '-'} -
- ); - }, - }, - ], - }, - { - header: 'Status Approval', - cell: (props) => { - const approval = props.row.original.approval; - if (!approval) return '-'; - - const status = approval.action; - const statusColor = getStatusBadgeColor(status); - const statusText = getStatusText(status); - - return ( - - ); - }, - }, - { - header: 'Catatan Approval', - cell: (props) => { - const approval = props.row.original.approval; - if (!approval?.notes) return '-'; - - return ( -
-

{approval.notes}

-
- ); - }, - }, - // { - // header: 'Status Grading Telur', - // cell: (props) => { - // const status = props.row.original.egg_grading_status; - // if (!status) return '-'; - // const color = status === 'COMPLETED' ? 'success' : 'warning'; - // return ( - // - // {status} - // - // ); - // }, - // }, - { - header: 'Dibuat Oleh', - cell: (props) => props.row.original.created_user?.name || '-', - }, - { - header: 'Tanggal Submit', - cell: (props) => - formatDate(props.row.original.created_at, 'DD MMMM YYYY'), - }, - { - header: 'Aksi', - cell: (props: CellContext) => { - const currentPageSize = - props.table.getPaginationRowModel().rows.length; - const currentPageRows = - props.table.getPaginationRowModel().flatRows; - const currentRowRelativeIndex = - currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - - const deleteClickHandler = () => { - setSelectedRecording(props.row.original); - singleDeleteModal.openModal(); - }; - - const approveClickHandler = () => { - setRowSelection({ - [String(props.row.original.id)]: true, - }); - setApprovalNotes(''); - approveModal.openModal(); - }; - - const rejectClickHandler = () => { - setRowSelection({ - [String(props.row.original.id)]: true, - }); - setApprovalNotes(''); - rejectModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(recordings) ? recordings?.meta?.page : 0} - totalItems={ - isResponseSuccess(recordings) ? recordings?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} + {/* Filter Modal */} + + > + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + + + + + + +
+ + {/* Modal Footer */} +
+ + +
+
+
{ placeholder='(Opsional) Tambahkan catatan untuk reject ini...' rows={3} /> -
+ ); }; diff --git a/src/components/pages/production/recording/filter/RecordingFilter.ts b/src/components/pages/production/recording/filter/RecordingFilter.ts new file mode 100644 index 00000000..955ae744 --- /dev/null +++ b/src/components/pages/production/recording/filter/RecordingFilter.ts @@ -0,0 +1,15 @@ +import { string, object } from 'yup'; + +export const RecordingFilterSchema = object().shape({ + area_id: string().nullable(), + location_id: string().nullable(), + kandang_id: string().nullable(), + project_flock_kandang_id: string().nullable(), +}); + +export type RecordingFilterType = { + area_id: string | null; + location_id: string | null; + kandang_id: string | null; + project_flock_kandang_id: string | null; +}; diff --git a/src/components/pages/production/recording/skeleton/RecordingTableSkeleton.tsx b/src/components/pages/production/recording/skeleton/RecordingTableSkeleton.tsx new file mode 100644 index 00000000..dc96f7c4 --- /dev/null +++ b/src/components/pages/production/recording/skeleton/RecordingTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Recording } from '@/types/api/production/recording'; +import { ColumnDef } from '@tanstack/react-table'; + +const RecordingTableSkeleton = ({ + columns, + icon, + title = 'Data Recording Belum Tersedia', + subtitle = 'Tidak ada data recording untuk saat ini.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+ +
+ +
+ + ); +}; + +export default RecordingTableSkeleton; From a5b4deaac4d34586057eab866c9ab04baaa8d35f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 24 Feb 2026 15:13:37 +0700 Subject: [PATCH 012/149] refactor(FE): Refactor RecordingTable layout for improved readability --- src/app/production/recording/page.tsx | 2 +- .../production/recording/RecordingTable.tsx | 241 +++++++++--------- 2 files changed, 122 insertions(+), 121 deletions(-) diff --git a/src/app/production/recording/page.tsx b/src/app/production/recording/page.tsx index fbbac7cb..368a59ea 100644 --- a/src/app/production/recording/page.tsx +++ b/src/app/production/recording/page.tsx @@ -2,7 +2,7 @@ import RecordingTable from '@/components/pages/production/recording/RecordingTab const Recording = () => { return ( -
+
); diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index bd26670d..13737cf3 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1173,131 +1173,132 @@ const RecordingTable = () => { return ( <>
-
- {/* Action Buttons, Search and Filter Section */} -
- {/* Action Buttons */} -
- - - - - {selectedRowIds.length > 0 && ( - <> - - - - - - - - - )} -
- - {/* Search and Filter */} -
- - } - className={{ - wrapper: 'w-full min-w-24 max-w-3xs', - inputWrapper: 'rounded-xl! shadow-button-soft', - input: - 'placeholder:font-semibold placeholder:text-base-content/50', - }} - /> - +
+ {/* Action Buttons */} +
+ + - -
+ {selectedRowIds.length > 0 && ( + <> +
+ + + + + + + + + + )}
- {/* Table Section */} + {/* Search and Filter */} +
+ + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> + + + + +
+
+ + {/* Table Section */} +
{isLoading ? (
@@ -1335,7 +1336,7 @@ const RecordingTable = () => { rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn('mt-3 mb-0', { + containerClassName: cn('p-3 mb-0', { 'w-full': isResponseSuccess(recordings) && recordings?.data?.length === 0, From 9a5e2987d5de84f7fc76138a0c1e627d9c058e8d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 24 Feb 2026 15:22:06 +0700 Subject: [PATCH 013/149] refactor(FE): Conditionally load data based on filter modal state --- .../pages/production/recording/RecordingTable.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 13737cf3..752ae1d1 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -313,7 +313,7 @@ const RecordingTable = () => { isLoadingOptions: isLoadingLocationOptions, loadMore: loadMoreLocations, } = useSelect( - LocationApi.basePath, + filterModal.open ? LocationApi.basePath : null, 'id', 'name', 'search', @@ -325,7 +325,12 @@ const RecordingTable = () => { options: areaOptions, isLoadingOptions: isLoadingAreaOptions, loadMore: loadMoreAreas, - } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + } = useSelect( + filterModal.open ? AreaApi.basePath : null, + 'id', + 'name', + 'search' + ); const projectFlockParams = useMemo(() => { if (filterProjectFlockLocationId) { @@ -341,7 +346,7 @@ const RecordingTable = () => { isLoadingOptions: isLoadingProjectFlocks, loadMore: loadMoreProjectFlocks, } = useSelect( - ProjectFlockApi.basePath, + filterModal.open ? ProjectFlockApi.basePath : null, 'id', 'flock_name', 'search', From bdca10e0ac79b5f64f28429870c34846856fad61 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 24 Feb 2026 15:31:22 +0700 Subject: [PATCH 014/149] refactor(FE): Handle rejected recordings in selection logic --- .../pages/production/recording/RecordingTable.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 752ae1d1..eea30412 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -701,7 +701,11 @@ const RecordingTable = () => { const allRows = table.getRowModel().rows; const selectableRows = allRows.filter((row) => { const recording = row.original; - return !isRecordingApproved(recording); + const isRecordingApproved = recording.approval?.action === 'APPROVED' && + recording.approval?.step_number === 2 && + recording.approval?.step_name === 'Disetujui'; + const isRecordingRejected = recording.approval?.action === 'REJECTED'; + return !isRecordingApproved && !isRecordingRejected; }); const hasNoSelectableRows = selectableRows.length === 0; @@ -738,7 +742,11 @@ const RecordingTable = () => { }, cell: ({ row }) => { const recording = row.original; - const isDisabled = isRecordingApproved(recording); + const isRecordingApproved = recording.approval?.action === 'APPROVED' && + recording.approval?.step_number === 2 && + recording.approval?.step_name === 'Disetujui'; + const isRecordingRejected = recording.approval?.action === 'REJECTED'; + const isDisabled = isRecordingApproved || isRecordingRejected; const handleToggleSelection = (e: unknown) => { if (!isDisabled) { From 9de31c991dc7f50f30ffff4522ad03ab081bcc84 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 24 Feb 2026 15:34:11 +0700 Subject: [PATCH 015/149] refactor(FE): Refactor approval checks for readability --- .../pages/production/recording/RecordingTable.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index eea30412..d7f69229 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -701,10 +701,12 @@ const RecordingTable = () => { const allRows = table.getRowModel().rows; const selectableRows = allRows.filter((row) => { const recording = row.original; - const isRecordingApproved = recording.approval?.action === 'APPROVED' && + const isRecordingApproved = + recording.approval?.action === 'APPROVED' && recording.approval?.step_number === 2 && recording.approval?.step_name === 'Disetujui'; - const isRecordingRejected = recording.approval?.action === 'REJECTED'; + const isRecordingRejected = + recording.approval?.action === 'REJECTED'; return !isRecordingApproved && !isRecordingRejected; }); @@ -742,7 +744,8 @@ const RecordingTable = () => { }, cell: ({ row }) => { const recording = row.original; - const isRecordingApproved = recording.approval?.action === 'APPROVED' && + const isRecordingApproved = + recording.approval?.action === 'APPROVED' && recording.approval?.step_number === 2 && recording.approval?.step_name === 'Disetujui'; const isRecordingRejected = recording.approval?.action === 'REJECTED'; From ced3970aaed5c03061c0a569272da0b5dd60bc07 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 24 Feb 2026 15:38:19 +0700 Subject: [PATCH 016/149] refactor(FE): Add padding to RecordingTableSkeleton container class --- .../production/recording/skeleton/RecordingTableSkeleton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/skeleton/RecordingTableSkeleton.tsx b/src/components/pages/production/recording/skeleton/RecordingTableSkeleton.tsx index dc96f7c4..b4e4d0f7 100644 --- a/src/components/pages/production/recording/skeleton/RecordingTableSkeleton.tsx +++ b/src/components/pages/production/recording/skeleton/RecordingTableSkeleton.tsx @@ -23,7 +23,7 @@ const RecordingTableSkeleton = ({ className={{ skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4', headerColumnClassName: 'whitespace-nowrap', - containerClassName: 'mb-0 overflow-hidden', + containerClassName: 'mb-0 overflow-hidden p-3', tableWrapperClassName: 'overflow-hidden', }} /> From 1a2e38568b6256afcba1eab75a8d274defb713c9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 24 Feb 2026 15:46:32 +0700 Subject: [PATCH 017/149] refactor(FE): Remove unused page size selector and related logic --- .../production/recording/RecordingTable.tsx | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index d7f69229..120c1e62 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -34,7 +34,6 @@ import { RecordingFilterType, } from '@/components/pages/production/recording/filter/RecordingFilter'; import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton'; -import { ROWS_OPTIONS } from '@/config/constant'; import Table from '@/components/Table'; import { type Recording } from '@/types/api/production/recording'; import { RecordingApi } from '@/services/api/production'; @@ -583,15 +582,6 @@ const RecordingTable = () => { [updateFilter, setSearchValue, setPage] ); - const pageSizeChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - setPage(1); - }, - [setPageSize, setPage] - ); - const singleDeleteHandler = async () => { setIsDeleteLoading(true); @@ -1297,19 +1287,6 @@ const RecordingTable = () => { )} - -
@@ -1346,6 +1323,7 @@ const RecordingTable = () => { : 0 } onPageChange={setPage} + onPageSizeChange={setPageSize} isLoading={isLoading} sorting={sorting} setSorting={setSorting} From 6566b881b2a8f5c2a16f7c308da32268819916d5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 24 Feb 2026 21:02:56 +0700 Subject: [PATCH 018/149] refactor(FE): Use `formatTitleCase` for category text in StatusBadge --- .../pages/production/recording/RecordingTable.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 120c1e62..cd7002a9 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -10,7 +10,7 @@ import React, { import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table'; -import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { cn, formatDate, formatNumber, formatTitleCase } from '@/lib/helper'; import RequirePermission from '@/components/helper/RequirePermission'; import Modal, { useModal } from '@/components/Modal'; import Button from '@/components/Button'; @@ -790,11 +790,7 @@ const RecordingTable = () => { props.row.original.project_flock?.project_flock_category; if (!category) return '-'; const color = category === 'LAYING' ? 'info' : 'warning'; - return ( - - {category} - - ); + return ; }, }, { From f0041ca9384b1d24c65b3f3ee487f0ff719ee020 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 24 Feb 2026 21:05:22 +0700 Subject: [PATCH 019/149] refactor(FE): Filter inactive kandangs in UniformityForm options --- .../pages/production/uniformity/form/UniformityForm.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 724f7b81..6cbb134e 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -155,9 +155,12 @@ const UniformityForm = ({ const kandangOpts = selectedProjectFlockData.kandangs .filter((kandang: Kandang) => { if (formType === 'add') { - return approvedKandangIds.includes(kandang.id); + return ( + approvedKandangIds.includes(kandang.id) && + kandang.status === 'ACTIVE' + ); } - return true; + return kandang.status === 'ACTIVE'; }) .map((kandang: Kandang) => ({ value: kandang.id, From cce84a3a6f8f8ea5c35666e9ae76fc43c09d4796 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 09:46:19 +0700 Subject: [PATCH 020/149] refactor(FE): Refactor ButtonFilter to support excluded fields with useMemo --- src/components/helper/ButtonFilter.tsx | 30 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx index cff1d167..b403a83f 100644 --- a/src/components/helper/ButtonFilter.tsx +++ b/src/components/helper/ButtonFilter.tsx @@ -3,15 +3,33 @@ import { getFilledFormikValuesCount } from '@/lib/formik-helper'; import { cn } from '@/lib/helper'; import { Icon } from '@iconify/react'; import { FormikValues } from 'formik'; +import { useMemo } from 'react'; export type ButtonFilterProps = ButtonProps & { values: FormikValues; onClick: () => void; + excludeFields?: string[]; }; // 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200 -const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => { +const ButtonFilter = ({ + values, + onClick, + excludeFields = [], + ...props +}: ButtonFilterProps) => { + const filteredValues = useMemo(() => { + const result: FormikValues = {}; + Object.keys(values).forEach((key) => { + if (!excludeFields.includes(key)) { + result[key] = values[key]; + } + }); + return result; + }, [values, excludeFields]); + + const activeCount = getFilledFormikValuesCount(filteredValues); return ( From 47a243977790bdb768ac4c969641852c3dbce8a6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 10:07:26 +0700 Subject: [PATCH 021/149] feat(FE): Add support for grouped fields in ButtonFilter active count calculation --- src/components/helper/ButtonFilter.tsx | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx index b403a83f..4ea9bb4f 100644 --- a/src/components/helper/ButtonFilter.tsx +++ b/src/components/helper/ButtonFilter.tsx @@ -9,6 +9,7 @@ export type ButtonFilterProps = ButtonProps & { values: FormikValues; onClick: () => void; excludeFields?: string[]; + fieldGroups?: string[][]; }; // 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200 @@ -17,19 +18,36 @@ const ButtonFilter = ({ values, onClick, excludeFields = [], + fieldGroups = [], ...props }: ButtonFilterProps) => { - const filteredValues = useMemo(() => { - const result: FormikValues = {}; + const activeCount = useMemo(() => { + const filteredValues: FormikValues = {}; Object.keys(values).forEach((key) => { if (!excludeFields.includes(key)) { - result[key] = values[key]; + filteredValues[key] = values[key]; } }); - return result; - }, [values, excludeFields]); - const activeCount = getFilledFormikValuesCount(filteredValues); + let count = getFilledFormikValuesCount(filteredValues); + + fieldGroups.forEach((group) => { + const groupFields = group.filter( + (field) => !excludeFields.includes(field) + ); + const filledGroupFields = groupFields.filter( + (field) => filteredValues[field] + ); + if ( + filledGroupFields.length === groupFields.length && + groupFields.length > 1 + ) { + count -= groupFields.length - 1; + } + }); + + return count; + }, [values, excludeFields, fieldGroups]); return ( + className='px-3 py-2.5' + />
diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 0bf00833..de43ba68 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -30,6 +30,7 @@ import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import StatusBadge from '@/components/helper/StatusBadge'; import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter'; +import ButtonFilter from '@/components/helper/ButtonFilter'; const RowsOptionsMenu = ({ props, @@ -214,32 +215,6 @@ const MarketingTable = () => { updateFilter('customer_id', ''); }; - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - // Product filter - if (tableFilterState.product_ids) { - count += 1; - } - - // Status filter - if (tableFilterState.status) { - count += 1; - } - - // Customer filter - if (tableFilterState.customer_id) { - count += 1; - } - - return count; - }, [ - tableFilterState.product_ids, - tableFilterState.status, - tableFilterState.customer_id, - ]); - const approveClickHandler = () => { setApproveAction('APPROVED'); confirmationModal.openModal(); @@ -588,28 +563,14 @@ const MarketingTable = () => { )}
- + className='px-3 py-2.5' + /> void }) => { ); }, [formik.values.period, periodOptions]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - if (tableFilterState.area_id) count += 1; - if (tableFilterState.location_id) count += 1; - if (tableFilterState.kandang_id) count += 1; - if (tableFilterState.category) count += 1; - if (tableFilterState.period) count += 1; - return count; - }, [ - tableFilterState.area_id, - tableFilterState.location_id, - tableFilterState.kandang_id, - tableFilterState.category, - tableFilterState.period, - ]); - - const hasFilters = activeFiltersCount > 0; - // ===== FILTER DEPENDENCY HANDLERS ===== const handleFilterAreaChange = (area: OptionType | null) => { const areaId = area?.value ? String(area.value) : undefined; @@ -961,25 +943,12 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { }} /> - + className='px-3 py-2.5' + /> = { @@ -511,36 +512,6 @@ const RecordingTable = () => { ); }, [formik.values.kandang_id, kandangOptions]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (tableFilterState.areaFilter) { - count += 1; - } - - if (tableFilterState.locationFilter) { - count += 1; - } - - if (tableFilterState.kandangFilter) { - count += 1; - } - - if (tableFilterState.projectFlockKandangFilter) { - count += 1; - } - - return count; - }, [ - tableFilterState.areaFilter, - tableFilterState.locationFilter, - tableFilterState.kandangFilter, - tableFilterState.projectFlockKandangFilter, - ]); - - const hasFilters = activeFiltersCount > 0; - // ===== HANDLE FILTER MODAL OPEN ===== const handleFilterModalOpen = () => { filterModal.openModal(); @@ -1264,25 +1235,12 @@ const RecordingTable = () => { }} /> - + className='px-3 py-2.5' + />
diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index bf4c31e3..9a9813f0 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -34,6 +34,7 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Color } from '@/types/theme'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import ButtonFilter from '@/components/helper/ButtonFilter'; const RowOptionsMenu = ({ props, @@ -159,30 +160,6 @@ const TransferToLayingsTable = () => { TransferToLayingApi.getAllFetcher ); - const filterCount = useMemo(() => { - let count = 0; - - if (tableFilterState.startDate && tableFilterState.endDate) { - count += 1; - } - - if (tableFilterState.flockSource.length > 0) { - count += 1; - } - - if (tableFilterState.flockDestination.length > 0) { - count += 1; - } - - if (tableFilterState.status.length > 0) { - count += 1; - } - - return count; - }, [tableFilterState]); - - const isFilterActive = filterCount > 0; - const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); @@ -559,30 +536,19 @@ const TransferToLayingsTable = () => { }} /> - + className='px-3 py-2.5' + /> { const { state: tableFilterState, + updateFilter, setPage, + setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { search: '', + start_date: '', + end_date: '', + location_id: '', + project_flock_id: '', + kandang_id: '', }, paramMap: { page: 'page', pageSize: 'limit', search: 'search', + start_date: 'start_date', + end_date: 'end_date', + location_id: 'location_id', + project_flock_id: 'project_flock_id', + kandang_id: 'kandang_id', }, }); @@ -233,8 +246,6 @@ const UniformityTable = () => { const [filterKandang, setFilterKandang] = useState(null); const [filterProjectFlockKandangId, setFilterProjectFlockKandangId] = useState(undefined); - const [filterStartDate, setFilterStartDate] = useState(''); - const [filterEndDate, setFilterEndDate] = useState(''); const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] = useState(''); const [, setFilterErrors] = useState>({}); @@ -319,8 +330,8 @@ const UniformityTable = () => { // ===== FORMIK FILTER ===== const filterFormik = useFormik({ initialValues: { - start_date: filterStartDate, - end_date: filterEndDate, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, location: filterLocation, project_flock: filterProjectFlock, project_flock_kandang_id: filterProjectFlockKandangId, @@ -329,8 +340,21 @@ const UniformityTable = () => { validationSchema: UniformityTableFilterSchema, enableReinitialize: true, onSubmit: async (values) => { - setFilterStartDate(values.start_date); - setFilterEndDate(values.end_date); + updateFilter('start_date', values.start_date); + updateFilter('end_date', values.end_date); + updateFilter( + 'location_id', + values.location?.value ? String(values.location.value) : '' + ); + updateFilter( + 'project_flock_id', + values.project_flock?.value ? String(values.project_flock.value) : '' + ); + updateFilter( + 'kandang_id', + values.kandang?.value ? String(values.kandang.value) : '' + ); + setFilterLocation(values.location ?? null); setFilterProjectFlock(values.project_flock ?? null); setFilterKandang(values.kandang ?? null); @@ -356,11 +380,11 @@ const UniformityTable = () => { filterProjectFlockKandangId.toString() ); } - if (filterStartDate) { - queryParams.append('start_date', filterStartDate); + if (tableFilterState.start_date) { + queryParams.append('start_date', tableFilterState.start_date); } - if (filterEndDate) { - queryParams.append('end_date', filterEndDate); + if (tableFilterState.end_date) { + queryParams.append('end_date', tableFilterState.end_date); } queryParams.append('with_chart', 'true'); } @@ -379,8 +403,8 @@ const UniformityTable = () => { }, [ isSubmitted, filterProjectFlockKandangId, - filterStartDate, - filterEndDate, + tableFilterState.start_date, + tableFilterState.end_date, getTableFilterQueryString, ]); @@ -456,30 +480,16 @@ const UniformityTable = () => { setFilterProjectFlock(null); setFilterKandang(null); setFilterProjectFlockKandangId(undefined); - setFilterStartDate(''); - setFilterEndDate(''); setFilterErrors({}); + updateFilter('start_date', ''); + updateFilter('end_date', ''); + updateFilter('location_id', ''); + updateFilter('project_flock_id', ''); + updateFilter('kandang_id', ''); + filterFormik.resetForm(); - }, [filterFormik]); - - const handleFilterStartDateChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - setFilterStartDate(value); - filterFormik.setFieldValue('start_date', value); - }, - [filterFormik] - ); - - const handleFilterEndDateChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - setFilterEndDate(value); - filterFormik.setFieldValue('end_date', value); - }, - [filterFormik] - ); + }, [filterFormik, updateFilter]); const selectedRowIds = useMemo(() => { return Object.keys(rowSelection) @@ -662,11 +672,11 @@ const UniformityTable = () => { filterProjectFlockKandangId.toString() ); } - if (filterStartDate) { - queryParams.append('start_date', filterStartDate); + if (tableFilterState.start_date) { + queryParams.append('start_date', tableFilterState.start_date); } - if (filterEndDate) { - queryParams.append('end_date', filterEndDate); + if (tableFilterState.end_date) { + queryParams.append('end_date', tableFilterState.end_date); } queryParams.append('limit', '100'); queryParams.append('page', '1'); @@ -677,7 +687,7 @@ const UniformityTable = () => { const response = await UniformityApi.getAllFetcher(url); return isResponseSuccess(response) ? response.data : null; - }, [filterProjectFlockKandangId, filterStartDate, filterEndDate]); + }, [filterProjectFlockKandangId, tableFilterState.start_date, tableFilterState.end_date]); const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); @@ -698,8 +708,8 @@ const UniformityTable = () => { location_name: locationName, project_flock_name: projectFlockName, kandang_name: kandangName, - start_date: filterStartDate, - end_date: filterEndDate, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, }); toast.success('Excel berhasil dibuat dan diunduh.'); @@ -713,8 +723,8 @@ const UniformityTable = () => { filterLocation, filterProjectFlock, filterKandang, - filterStartDate, - filterEndDate, + tableFilterState.start_date, + tableFilterState.end_date, ]); const handleExportPDF = useCallback(async () => { @@ -736,8 +746,8 @@ const UniformityTable = () => { location_name: locationName, project_flock_name: projectFlockName, kandang_name: kandangName, - start_date: filterStartDate, - end_date: filterEndDate, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, }); toast.success('PDF berhasil dibuat dan diunduh.'); @@ -751,8 +761,8 @@ const UniformityTable = () => { filterLocation, filterProjectFlock, filterKandang, - filterStartDate, - filterEndDate, + tableFilterState.start_date, + tableFilterState.end_date, ]); useEffect(() => { @@ -883,37 +893,6 @@ const UniformityTable = () => { [] ); - // ===== CALCULATE FILTER COUNT ===== - const filterCount = useMemo(() => { - let count = 0; - - if (filterStartDate && filterEndDate) { - count += 1; - } - - if (filterLocation) { - count += 1; - } - - if (filterProjectFlock) { - count += 1; - } - - if (filterKandang) { - count += 1; - } - - return count; - }, [ - filterStartDate, - filterEndDate, - filterLocation, - filterProjectFlock, - filterKandang, - ]); - - const isFilterActive = filterCount > 0; - return ( <>
@@ -932,30 +911,13 @@ const UniformityTable = () => {
- + className='px-3 py-2.5' + /> { placeholder='Tanggal Mulai' value={filterFormik.values.start_date} errorMessage={filterFormik.errors.start_date} - onChange={handleFilterStartDateChange} + onChange={(e) => filterFormik.setFieldValue('start_date', e.target.value)} isError={ filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) @@ -1291,7 +1253,7 @@ const UniformityTable = () => { placeholder='Tanggal Akhir' value={filterFormik.values.end_date} errorMessage={filterFormik.errors.end_date} - onChange={handleFilterEndDateChange} + onChange={(e) => filterFormik.setFieldValue('end_date', e.target.value)} isError={ filterFormik.touched.end_date && Boolean(filterFormik.errors.end_date) diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index 121338b7..b9296f89 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -38,6 +38,7 @@ import { Nonstock } from '@/types/api/master-data/nonstock'; import { ColumnDef } from '@tanstack/react-table'; import { httpClient } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; +import ButtonFilter from '@/components/helper/ButtonFilter'; interface ReportExpenseTabProps { tabId: string; @@ -169,20 +170,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { [formik.values.category] ); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - if (filterParams.location_id) count += 1; - if (filterParams.supplier_id) count += 1; - if (filterParams.kandang_id) count += 1; - if (filterParams.nonstock_id) count += 1; - if (filterParams.realization_date) count += 1; - if (filterParams.category) count += 1; - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: reportExpenseResponse, isLoading } = useSWR( isSubmitted @@ -312,25 +299,12 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, handleExportExcel, handleExportPDF, diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 9ee90fae..1e89ca43 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -38,6 +38,7 @@ import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { OptionType } from '@/components/table/TableRowSizeSelector'; import { Color } from '@/types/theme'; +import ButtonFilter from '@/components/helper/ButtonFilter'; interface CustomerPaymentTabProps { tabId: string; @@ -213,30 +214,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ); }, [formik.values.filter_by]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - // Date filter (start_date + end_date = 1 filter) - if (filterParams.start_date || filterParams.end_date) { - count += 1; - } - - // Customer filter - if (filterParams.customer_ids) { - count += 1; - } - - // Filter by type filter (hanya dihitung jika ada nilai yang dipilih) - if (filterParams.filter_by) { - count += 1; - } - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: customerPayment, isLoading } = useSWR( isSubmitted @@ -380,25 +357,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, handleExportExcel, handleExportPdf, diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index d1b7425d..56bf3259 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -274,6 +274,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
{ ); }, [formik.values.sort_by, sortByOptions]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (filterParams.start_date || filterParams.end_date) { - count += 1; - } - - if (filterParams.area_id) { - count += 1; - } - - if (filterParams.supplier_id) { - count += 1; - } - - if (filterParams.product_id) { - count += 1; - } - - if (filterParams.product_category_id) { - count += 1; - } - - if (filterParams.filter_by) { - count += 1; - } - - if (filterParams.sort_by) { - count += 1; - } - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( isSubmitted @@ -486,25 +450,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, filterModal.open, setTabActions, diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index eda89750..219a380d 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -47,6 +47,7 @@ import { MARKETING_TYPE_OPTIONS, } from '@/config/constant'; import Badge from '@/components/Badge'; +import ButtonFilter from '@/components/helper/ButtonFilter'; interface DailyMarketingTabProps { tabId: string; @@ -202,47 +203,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { ); }, [formik.values.marketing_type]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (filterParams.area_id) { - count += 1; - } - - if (filterParams.location_id) { - count += 1; - } - - if (filterParams.warehouse_id) { - count += 1; - } - - if (filterParams.customer_id) { - count += 1; - } - - if (filterParams.start_date || filterParams.end_date) { - count += 1; - } - - if (filterParams.filter_by) { - count += 1; - } - - if (filterParams.marketing_type) { - count += 1; - } - - if (filterParams.sort_by) { - count += 1; - } - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: dailyMarketings, isLoading } = useSWR( isSubmitted @@ -412,30 +372,13 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }} /> - + variant='outline' + className='px-3 py-2.5' + /> { }, [ tabId, searchValue, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, filterModal.open, setTabActions, diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index dc487003..2f784af8 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -17,6 +17,7 @@ import { } from '@/types/api/report/hpp-per-kandang'; import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; +import ButtonFilter from '@/components/helper/ButtonFilter'; import Dropdown from '@/components/Dropdown'; import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/export/HppPerkandangExportPDF'; import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX'; @@ -233,42 +234,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { ); }, [formik.values.show_unrecorded, showUnrecordedOptions]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (filterParams.period) { - count += 1; - } - - if (filterParams.area_id) { - count += 1; - } - - if (filterParams.location_id) { - count += 1; - } - - if (filterParams.kandang_id) { - count += 1; - } - - if (filterParams.weight_min || filterParams.weight_max) { - count += 1; - } - - if (filterParams.show_unrecorded !== undefined) { - count += 1; - } - - if (filterParams.sort_by) { - count += 1; - } - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; // ===== DATA FETCHING ===== const { data: hppPerKandang, isLoading } = useSWR( @@ -486,25 +451,12 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + formik.values, isAnyExportLoading, filterModal.open, setTabActions, diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index cbefadfe..20dddff1 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -7,6 +7,7 @@ import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; +import ButtonFilter from '@/components/helper/ButtonFilter'; import Dropdown from '@/components/dropdown/Dropdown'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable'; @@ -324,20 +325,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { [formik.values.kandang_id] ); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; - - if (filterParams.area_id) count += 1; - if (filterParams.location_id) count += 1; - if (filterParams.project_flock_id) count += 1; - if (filterParams.project_flock_kandang_id) count += 1; - - return count; - }, [filterParams]); - - const hasFilters = activeFiltersCount > 0; - // ===== DATA FETCHING ===== const { data: projectFlockKandangsData, isLoading } = useSWR< BaseApiResponse @@ -539,25 +526,12 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { setTabActions( tabId,
- + variant='outline' + className='px-3 py-2.5' + /> { ); }, [ tabId, - hasFilters, - activeFiltersCount, + filterParams, isAnyExportLoading, exportToExcelHandler, exportToPdfHandler, From dae9a24a7c7e860b5db93db8bce4651dba0d5f3d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 10:41:34 +0700 Subject: [PATCH 023/149] refactor(FE): Refactor array formatting and improve code readability --- .../production/uniformity/UniformityTable.tsx | 14 +++++++++++--- .../report/marketing/tab/HppPerKandangTab.tsx | 1 - 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index c28a8a43..f45e803d 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -687,7 +687,11 @@ const UniformityTable = () => { const response = await UniformityApi.getAllFetcher(url); return isResponseSuccess(response) ? response.data : null; - }, [filterProjectFlockKandangId, tableFilterState.start_date, tableFilterState.end_date]); + }, [ + filterProjectFlockKandangId, + tableFilterState.start_date, + tableFilterState.end_date, + ]); const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); @@ -1241,7 +1245,9 @@ const UniformityTable = () => { placeholder='Tanggal Mulai' value={filterFormik.values.start_date} errorMessage={filterFormik.errors.start_date} - onChange={(e) => filterFormik.setFieldValue('start_date', e.target.value)} + onChange={(e) => + filterFormik.setFieldValue('start_date', e.target.value) + } isError={ filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) @@ -1253,7 +1259,9 @@ const UniformityTable = () => { placeholder='Tanggal Akhir' value={filterFormik.values.end_date} errorMessage={filterFormik.errors.end_date} - onChange={(e) => filterFormik.setFieldValue('end_date', e.target.value)} + onChange={(e) => + filterFormik.setFieldValue('end_date', e.target.value) + } isError={ filterFormik.touched.end_date && Boolean(filterFormik.errors.end_date) diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 2f784af8..953b4a88 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -234,7 +234,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { ); }, [formik.values.show_unrecorded, showUnrecordedOptions]); - // ===== DATA FETCHING ===== const { data: hppPerKandang, isLoading } = useSWR( isSubmitted From 93a2d99b7f0b1753d2abe772af8df6892d47eee0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 10:52:03 +0700 Subject: [PATCH 024/149] refactor(FE): Disable filter button if kandang_id is not selected --- src/components/pages/production/recording/RecordingTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 7a7e32d8..5f5cb091 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1396,7 +1396,7 @@ const RecordingTable = () => { From 90942b41b9da86ff055afd408dbbc9bf6bb8e128 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 10:53:32 +0700 Subject: [PATCH 025/149] refactor(FE): Fix formatting of disabled condition in RecordingTable button --- .../pages/production/recording/RecordingTable.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 5f5cb091..688df7f4 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1396,7 +1396,11 @@ const RecordingTable = () => { From e2e64f093f28a8a3333c54bb9a08f57b0b8e353a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 11:02:26 +0700 Subject: [PATCH 026/149] refactor(FE): Refactor TransferToLayingFilterModal to use schema validation --- .../TransferToLayingFilterModal.tsx | 49 ++++++++++++------- .../TransferToLayingsTable.tsx | 3 +- .../filter/TransferToLayingFilter.ts | 33 +++++++++++++ 3 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 src/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter.ts diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx index db758762..95d8aa6c 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx @@ -13,6 +13,10 @@ import { OptionType, useSelect } from '@/components/input/SelectInput'; import { ProjectFlockApi } from '@/services/api/production'; import { Flock } from '@/types/api/master-data/flock'; import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying'; +import { + TransferToLayingFilterSchema, + TransferToLayingFilterValues, +} from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter'; interface TransferToLayingFilterModal { ref: RefObject; @@ -49,13 +53,7 @@ const TransferToLayingFilterModal = ({ category: 'LAYING', }); - const formik = useFormik<{ - startDate: string; - endDate: string; - flockSource: { value: number; label: string }[]; - flockDestination: { value: number; label: string }[]; - status: { value: number; label: string }[]; - }>({ + const formik = useFormik({ initialValues: { startDate: '', endDate: '', @@ -63,15 +61,22 @@ const TransferToLayingFilterModal = ({ flockDestination: [], status: [], }, + validationSchema: TransferToLayingFilterSchema, onSubmit: async (values) => { const formattedValues = { ...values, - flockSource: values.flockSource.map((item) => item.value), - flockDestination: values.flockDestination.map((item) => item.value), - status: values.status.map((item) => item.value), + flockSource: values.flockSource + ? (values.flockSource as OptionType[]).map((item) => item.value) + : [], + flockDestination: values.flockDestination + ? (values.flockDestination as OptionType[]).map((item) => item.value) + : [], + status: values.status + ? (values.status as OptionType[]).map((item) => item.value) + : [], }; - onSubmit?.(formattedValues); + onSubmit?.(formattedValues as TransferToLayingFilter); closeModalHandler(); }, onReset: () => { @@ -81,17 +86,17 @@ const TransferToLayingFilterModal = ({ }); const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('flockSource', val as OptionType[]); + formik.setFieldValue('flockSource', val); }; const flockDestinationChangeHandler = ( val: OptionType | OptionType[] | null ) => { - formik.setFieldValue('flockDestination', val as OptionType[]); + formik.setFieldValue('flockDestination', val); }; const statusChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('status', val as OptionType[]); + formik.setFieldValue('status', val); }; return ( @@ -132,7 +137,7 @@ const TransferToLayingFilterModal = ({ @@ -140,16 +145,22 @@ const TransferToLayingFilterModal = ({
+ {formik.touched.endDate && formik.errors.endDate && ( + + {formik.errors.endDate} + + )}
diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index 9a9813f0..06852fe1 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; +import { ChangeEventHandler, useEffect, useState } from 'react'; import useSWR from 'swr'; import { CellContext, @@ -17,7 +17,6 @@ import { useModal } from '@/components/Modal'; import CheckboxInput from '@/components/input/CheckboxInput'; import RequirePermission from '@/components/helper/RequirePermission'; import PopoverButton from '@/components/popover/PopoverButton'; -import Badge from '@/components/Badge'; import PopoverContent from '@/components/popover/PopoverContent'; import Dropdown from '@/components/Dropdown'; import StatusBadge from '@/components/helper/StatusBadge'; diff --git a/src/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter.ts b/src/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter.ts new file mode 100644 index 00000000..bc402d29 --- /dev/null +++ b/src/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter.ts @@ -0,0 +1,33 @@ +import * as yup from 'yup'; + +export type TransferToLayingFilterType = { + startDate: string | null; + endDate: string | null; + flockSource: number[]; + flockDestination: number[]; + status: string[]; +}; + +export const TransferToLayingFilterSchema = yup.object({ + startDate: yup.string().optional().nullable(), + endDate: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { startDate } = this.parent; + if (!startDate || !value) return true; + return new Date(value) >= new Date(startDate); + } + ), + flockSource: yup.array().optional().nullable(), + flockDestination: yup.array().optional().nullable(), + status: yup.array().optional().nullable(), +}); + +export type TransferToLayingFilterValues = yup.InferType< + typeof TransferToLayingFilterSchema +>; From 22b1102454b00d79fb72c853d82fb3572d742a74 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 11:40:42 +0700 Subject: [PATCH 027/149] refactor(FE): Refactor ExpensesTable to use ExpensesFilterModal --- .../pages/expense/ExpensesTable.tsx | 406 ++++++++---------- .../pages/expense/filter/ExpensesFilter.ts | 28 ++ .../expense/filter/ExpensesFilterModal.tsx | 206 +++++++++ 3 files changed, 423 insertions(+), 217 deletions(-) create mode 100644 src/components/pages/expense/filter/ExpensesFilter.ts create mode 100644 src/components/pages/expense/filter/ExpensesFilterModal.tsx diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index e141ad67..5875bc0c 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -16,10 +16,6 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; @@ -27,17 +23,15 @@ import RealizationStatusBadge from '@/components/pages/expense/RealizationStatus import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; -import DateInput from '@/components/input/DateInput'; import RequirePermission from '@/components/helper/RequirePermission'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { LocationApi, SupplierApi } from '@/services/api/master-data'; -import { Location } from '@/types/api/master-data/location'; -import { Supplier } from '@/types/api/master-data/supplier'; import { BaseApiResponse } from '@/types/api/api-general'; const RowOptionsMenu = ({ @@ -179,6 +173,9 @@ const ExpensesTable = () => { const approveModal = useModal(); const rejectModal = useModal(); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + const [selectedExpense, setSelectedExpense] = useState( undefined ); @@ -535,51 +532,32 @@ const ExpensesTable = () => { setIsRejectLoading(false); }; - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - } = useSelect(LocationApi.basePath, 'id', 'name'); - - const [selectedLocation, setSelectedLocation] = useState( - null - ); - - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - updateFilter( - 'locationId', - val ? ((val as OptionType).value as string) : '' - ); - }; - - const { - setInputValue: setVendorInputValue, - options: vendorOptions, - isLoadingOptions: isLoadingVendorOptions, - } = useSelect(SupplierApi.basePath, 'id', 'name'); - - const [selectedVendor, setSelectedVendor] = useState(null); - - const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedVendor(val as OptionType); - updateFilter('vendorId', val ? ((val as OptionType).value as string) : ''); - }; - const searchChangeHandler: ChangeEventHandler = (e) => { updateFilter('search', e.target.value); }; - const transactionDateChangeHandler: ChangeEventHandler = ( - e - ) => { - updateFilter('transactionDate', e.target.value); + // ===== FILTER MODAL HANDLERS ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); }; - const realizationDateChangeHandler: ChangeEventHandler = ( - e - ) => { - updateFilter('realizationDate', e.target.value); + const handleFilterSubmit = (values: { + transaction_date?: string | null; + realization_date?: string | null; + location_id?: string | null; + vendor_id?: string | null; + }) => { + updateFilter('transactionDate', values.transaction_date || ''); + updateFilter('realizationDate', values.realization_date || ''); + updateFilter('locationId', values.location_id || ''); + updateFilter('vendorId', values.vendor_id || ''); + }; + + const handleFilterReset = () => { + updateFilter('transactionDate', ''); + updateFilter('realizationDate', ''); + updateFilter('locationId', ''); + updateFilter('vendorId', ''); }; // track sorting @@ -595,188 +573,176 @@ const ExpensesTable = () => { return ( <> -
-
-
-
-
- +
+
+ {/* Action Buttons */} +
+ + + + + {selectedRowIds.length > 0 && ( + <> +
+ + - {selectedRowIds.length > 0 && ( - <> - - - + + + - - - + + + - - - + + + + + )} +
- - - - - )} -
-
+ {/* Search and Filter */} +
+ + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> -
- - - - - - - - - -
+
- - data={isResponseSuccess(expenses) ? expenses?.data : []} - columns={expensesColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} - totalItems={ - isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - enableRowSelection={tableEnableRowSelectionHandler} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(expenses) && expenses?.data?.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - }} - /> + {/* Table Section */} +
+ + data={isResponseSuccess(expenses) ? expenses?.data : []} + columns={expensesColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} + totalItems={ + isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + enableRowSelection={tableEnableRowSelectionHandler} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(expenses) && expenses?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
{ onClick: confirmationModalRejectClickHandler, }} /> + + ); }; diff --git a/src/components/pages/expense/filter/ExpensesFilter.ts b/src/components/pages/expense/filter/ExpensesFilter.ts new file mode 100644 index 00000000..8ee14a90 --- /dev/null +++ b/src/components/pages/expense/filter/ExpensesFilter.ts @@ -0,0 +1,28 @@ +import * as yup from 'yup'; + +export type ExpensesFilterType = { + transaction_date: string | null; + realization_date: string | null; + location_id: string | null; + vendor_id: string | null; +}; + +export const ExpensesFilterSchema = yup.object({ + transaction_date: yup.string().nullable(), + realization_date: yup + .string() + .nullable() + .test( + 'is-greater-or-equal-transaction', + 'Tanggal realisasi tidak boleh sebelum tanggal transaksi', + function (value) { + const { transaction_date } = this.parent; + if (!transaction_date || !value) return true; + return new Date(value) >= new Date(transaction_date); + } + ), + location_id: yup.string().nullable(), + vendor_id: yup.string().nullable(), +}); + +export type ExpensesFilterValues = yup.InferType; diff --git a/src/components/pages/expense/filter/ExpensesFilterModal.tsx b/src/components/pages/expense/filter/ExpensesFilterModal.tsx new file mode 100644 index 00000000..99f5a75a --- /dev/null +++ b/src/components/pages/expense/filter/ExpensesFilterModal.tsx @@ -0,0 +1,206 @@ +'use client'; + +import { RefObject } from 'react'; +import { useFormik } from 'formik'; + +import { Icon } from '@iconify/react'; +import Modal from '@/components/Modal'; +import Button from '@/components/Button'; +import DateInput from '@/components/input/DateInput'; +import SelectInput from '@/components/input/SelectInput'; + +import { OptionType, useSelect } from '@/components/input/SelectInput'; +import { LocationApi, SupplierApi } from '@/services/api/master-data'; +import { Location } from '@/types/api/master-data/location'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { + ExpensesFilterSchema, + ExpensesFilterValues, +} from '@/components/pages/expense/filter/ExpensesFilter'; + +interface ExpensesFilterModalProps { + ref: RefObject; + initialValues?: ExpensesFilterValues; + onSubmit?: (values: Partial) => void; + onReset?: () => void; +} + +const ExpensesFilterModal = ({ + ref, + initialValues, + onSubmit, + onReset, +}: ExpensesFilterModalProps) => { + const closeModalHandler = () => { + ref.current?.close(); + }; + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setVendorInputValue, + options: vendorOptions, + isLoadingOptions: isLoadingVendorOptions, + } = useSelect(SupplierApi.basePath, 'id', 'name'); + + const formik = useFormik({ + initialValues: initialValues || { + transaction_date: null, + realization_date: null, + location_id: null, + vendor_id: null, + }, + validationSchema: ExpensesFilterSchema, + onSubmit: async (values) => { + onSubmit?.(values); + closeModalHandler(); + }, + onReset: () => { + onReset?.(); + closeModalHandler(); + }, + }); + + const locationValue = formik.values.location_id + ? locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + : null; + + const vendorValue = formik.values.vendor_id + ? vendorOptions.find( + (opt) => String(opt.value) === formik.values.vendor_id + ) || null + : null; + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + const locationId = + val && !Array.isArray(val) ? (String(val.value) as string) : null; + formik.setFieldValue('location_id', locationId); + }; + + const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { + const vendorId = + val && !Array.isArray(val) ? (String(val.value) as string) : null; + formik.setFieldValue('vendor_id', vendorId); + }; + + return ( + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ + +
+ + {/* Modal Body */} +
+ + + + {formik.touched.realization_date && + formik.errors.realization_date && ( + + {formik.errors.realization_date} + + )} + + + + +
+ + {/* Modal Footer */} +
+ + + +
+ +
+ ); +}; + +export default ExpensesFilterModal; From 4fda2f661a167b0e36a8205ef65758731e7389ae Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 11:49:41 +0700 Subject: [PATCH 028/149] refactor(FE): Remove unnecessary padding and margin classes in components --- src/app/production/recording/page.tsx | 2 +- src/components/pages/expense/ExpensesTable.tsx | 2 +- src/components/pages/production/recording/RecordingTable.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/production/recording/page.tsx b/src/app/production/recording/page.tsx index 368a59ea..9b986b49 100644 --- a/src/app/production/recording/page.tsx +++ b/src/app/production/recording/page.tsx @@ -2,7 +2,7 @@ import RecordingTable from '@/components/pages/production/recording/RecordingTab const Recording = () => { return ( -
+
); diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 5875bc0c..ba6e4ada 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -717,7 +717,7 @@ const ExpensesTable = () => {
{/* Table Section */} -
+
data={isResponseSuccess(expenses) ? expenses?.data : []} columns={expensesColumns} diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 688df7f4..8f59d7c3 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1245,7 +1245,7 @@ const RecordingTable = () => {
{/* Table Section */} -
+
{isLoading ? (
From 8be33b230bf97f80d512610a13bcada233c9fd28 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 11:59:17 +0700 Subject: [PATCH 029/149] refactor(FE): Refactor row options menu to use popover components --- .../pages/expense/ExpensesTable.tsx | 202 ++++++++---------- 1 file changed, 88 insertions(+), 114 deletions(-) diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index ba6e4ada..e2e86535 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -16,9 +16,8 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; @@ -35,16 +34,21 @@ import { useTableFilter } from '@/services/hooks/useTableFilter'; import { BaseApiResponse } from '@/types/api/api-general'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; - approveClickHandler: () => void; - rejectClickHandler: () => void; deleteClickHandler: () => void; }) => { + const popoverId = `expense#${props.row.original.id}`; + const popoverAnchorName = `--anchor-expense#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + const showEditButton = props.row.original.latest_approval ? props.row.original.latest_approval.step_number !== 6 && (props.row.original.latest_approval.step_number === 1 || @@ -53,81 +57,95 @@ const RowOptionsMenu = ({ props.row.original.latest_approval.step_number === 4) : false; - // TODO: apply RBAC const showRealizationButton = props.row.original.latest_approval ? props.row.original.latest_approval.action !== 'REJECTED' && props.row.original.latest_approval.step_number === 4 : false; return ( - -
- - - +
+ + + - {showEditButton && ( - + +
+ - )} - {showRealizationButton && ( - + {showEditButton && ( + + + + )} + + {showRealizationButton && ( + + + + )} + + - )} - - - - -
- +
+ +
); }; @@ -337,31 +355,7 @@ const ExpensesTable = () => { const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; - - const approveClickHandler = () => { - setSelectedExpense(props.row.original); - - // Set row selection - setRowSelection({ - [String(props.row.original.id)]: true, - }); - - setApprovalNotes(''); - approveModal.openModal(); - }; - - const rejectClickHandler = () => { - setSelectedExpense(props.row.original); - - // Set row selection - setRowSelection({ - [String(props.row.original.id)]: true, - }); - - setApprovalNotes(''); - rejectModal.openModal(); - }; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; const deleteClickHandler = () => { setSelectedExpense(props.row.original); @@ -369,31 +363,11 @@ const ExpensesTable = () => { }; return ( - <> - {currentPageSize > 3 && ( - - - - )} - - {currentPageSize <= 3 && ( - - - - )} - + ); }, }, From 0af7b172a014b0c54f22e32a6fbc2f9b70de92b3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 12:08:31 +0700 Subject: [PATCH 030/149] refactor(FE): Refactor PurchaseTable to use Popover for row options menu --- .../pages/purchase/PurchaseTable.tsx | 252 ++++++++---------- 1 file changed, 115 insertions(+), 137 deletions(-) diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 733165f8..87992ad2 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -12,10 +12,9 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; @@ -69,59 +68,72 @@ const getStatusBadgeColor = (status: string): Color => { return statusBadgeColorMap[status] || 'neutral'; }; -// ===== INTERFACES ===== -interface RowOptionsMenuProps { - type: 'dropdown' | 'collapse'; - props: CellContext; - deleteClickHandler: () => void; -} - +// ===== ROW OPTIONS MENU ===== const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, -}: RowOptionsMenuProps) => { +}: { + popoverPosition: 'bottom' | 'top'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + const popoverId = `purchase#${props.row.original.id}`; + const popoverAnchorName = `--anchor-purchase#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - {/**/} - {/* */} - {/* Edit*/} - {/**/} + +
+ + + - - - - + + + +
+
+
); }; @@ -346,27 +358,11 @@ const PurchaseTable = () => { }; return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - + ); }, }, @@ -405,22 +401,22 @@ const PurchaseTable = () => { return ( <> -
-
-
-
- - - -
+
+
+
+ + + +
+
{ }} />
- -
- -
- - data={ - isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : [] - } - columns={purchaseColumns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(purchaseRequests) - ? purchaseRequests?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(purchaseRequests) - ? purchaseRequests?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(purchaseRequests) && - purchaseRequests?.data?.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - }} - /> + {/* Table Section */} +
+ + data={ + isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : [] + } + columns={purchaseColumns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(purchaseRequests) + ? purchaseRequests?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(purchaseRequests) + ? purchaseRequests?.meta?.total_results + : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3', { + 'w-full mb-20': + isResponseSuccess(purchaseRequests) && + purchaseRequests?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
{/* ===== MODAL COMPONENTS ===== */} From a75d84556a658e4dc39c5e07e58e6eba69cc8392 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 15:56:12 +0700 Subject: [PATCH 031/149] refactor(FE): Refactor date validation to use shared state and cleanup --- src/components/pages/finance/FinanceTable.tsx | 31 +++--- .../TransferToLayingFilterModal.tsx | 102 ++++++++++++++++-- .../production/uniformity/UniformityTable.tsx | 95 ++++++++++++++-- .../finance/filter/DebtSupplierFilter.ts | 14 ++- .../report/finance/tab/CustomerPaymentTab.tsx | 9 +- .../report/finance/tab/DebtSupplierTab.tsx | 78 +++++++++++++- .../tab/PurchasesPerSupplierTab.tsx | 9 +- .../marketing/tab/DailyMarketingTab.tsx | 89 ++++++++++++--- 8 files changed, 371 insertions(+), 56 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index f83fa469..10d959b9 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -189,6 +189,7 @@ const FinanceTable = () => { const [selectedFinance, setSelectedFinance] = useState(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); // ===== Formik for Filter ===== const filterFormik = useFormik({ @@ -335,10 +336,7 @@ const FinanceTable = () => { const endDateObj = new Date(endDate); if (endDateObj < startDate) { - filterFormik.setFieldError( - 'end_date', - 'Tanggal akhir tidak boleh masa lampau' - ); + setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, @@ -346,12 +344,14 @@ const FinanceTable = () => { setDateErrorShown(true); } } else { - filterFormik.setFieldError('end_date', undefined); + setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } } + } else { + setHasDateError(false); } }; @@ -366,10 +366,7 @@ const FinanceTable = () => { const endDate = new Date(value); if (endDate < startDateObj) { - filterFormik.setFieldError( - 'end_date', - 'Tanggal akhir tidak boleh masa lampau' - ); + setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, @@ -380,7 +377,7 @@ const FinanceTable = () => { } } - filterFormik.setFieldError('end_date', undefined); + setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); @@ -661,22 +658,20 @@ const FinanceTable = () => { name='start_date' label='Periode Tanggal (Mulai)' value={filterFormik.values.start_date} + errorMessage={filterFormik.errors.start_date} onChange={startDateChangeHandler} - errorMessage={ - filterFormik.errors.end_date - ? filterFormik.errors.end_date - : undefined + isError={ + filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) } /> { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + // ===== CLEANUP TOAST WHEN MODAL CLOSES ===== + useEffect(() => { + const dialogElement = ref.current; + const handleModalClose = () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + + dialogElement?.addEventListener('close', handleModalClose); + + return () => { + dialogElement?.removeEventListener('close', handleModalClose); + }; + }, [ref, dateErrorShown]); + // Flock Source const { setInputValue: setFlockSourceInputValue, @@ -138,24 +169,77 @@ const TransferToLayingFilterModal = ({ name='startDate' placeholder='Tanggal Awal' value={formik.values.startDate || ''} - onChange={formik.handleChange} + errorMessage={formik.errors.startDate} + onChange={(e) => { + const value = e.target.value; + formik.setFieldValue('startDate', value); + + if (value && formik.values.endDate) { + const startDate = new Date(value); + const endDateObj = new Date(formik.values.endDate); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }} onBlur={formik.handleBlur} + isError={ + formik.touched.startDate && Boolean(formik.errors.startDate) + } />
{ + const value = e.target.value; + formik.setFieldValue('endDate', value); + + if (value && formik.values.startDate) { + const startDateObj = new Date(formik.values.startDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }} onBlur={formik.handleBlur} - isError={formik.touched.endDate && !!formik.errors.endDate} + isError={ + (formik.touched.endDate && Boolean(formik.errors.endDate)) || hasDateError + } />
- {formik.touched.endDate && formik.errors.endDate && ( - - {formik.errors.endDate} - - )} { useState(''); const [, setFilterErrors] = useState>({}); + // ===== DATE ERROR STATE ===== + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); + const { setInputValue: setFilterLocationInputValue, options: filterLocationOptions, @@ -792,6 +796,23 @@ const UniformityTable = () => { } }, [uniformities, rowSelection]); + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + }, [filterModal.open, dateErrorShown]); + // ===== TABLE COLUMNS DEFINITION ===== const uniformityColumns: ColumnDef[] = useMemo( () => [ @@ -1245,9 +1266,38 @@ const UniformityTable = () => { placeholder='Tanggal Mulai' value={filterFormik.values.start_date} errorMessage={filterFormik.errors.start_date} - onChange={(e) => - filterFormik.setFieldValue('start_date', e.target.value) - } + onChange={(e) => { + const value = e.target.value; + filterFormik.setFieldValue('start_date', value); + + if (value && filterFormik.values.end_date) { + const startDate = new Date(value); + const endDateObj = new Date( + filterFormik.values.end_date + ); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error( + 'Tanggal akhir tidak boleh masa lampau', + { + duration: Infinity, + } + ); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }} isError={ filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) @@ -1259,13 +1309,38 @@ const UniformityTable = () => { placeholder='Tanggal Akhir' value={filterFormik.values.end_date} errorMessage={filterFormik.errors.end_date} - onChange={(e) => - filterFormik.setFieldValue('end_date', e.target.value) - } - isError={ - filterFormik.touched.end_date && - Boolean(filterFormik.errors.end_date) - } + onChange={(e) => { + const value = e.target.value; + filterFormik.setFieldValue('end_date', value); + + if (value && filterFormik.values.start_date) { + const startDateObj = new Date( + filterFormik.values.start_date + ); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error( + 'Tanggal akhir tidak boleh masa lampau', + { + duration: Infinity, + } + ); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }} + isError={hasDateError} />
diff --git a/src/components/pages/report/finance/filter/DebtSupplierFilter.ts b/src/components/pages/report/finance/filter/DebtSupplierFilter.ts index 1c1c2fac..0ecd299e 100644 --- a/src/components/pages/report/finance/filter/DebtSupplierFilter.ts +++ b/src/components/pages/report/finance/filter/DebtSupplierFilter.ts @@ -11,7 +11,19 @@ export type DebtSupplierFilterType = { export const DebtSupplierFilterSchema: yup.ObjectSchema = yup.object({ startDate: yup.string().optional().notRequired(), - endDate: yup.string().optional().notRequired(), + endDate: yup + .string() + .optional() + .notRequired() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const startDate = this.parent.startDate; + if (!startDate || !value) return true; + return new Date(value) >= new Date(startDate); + } + ), supplierIds: yup .array() .of( diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 1e89ca43..4b596155 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -844,19 +844,26 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 56bf3259..d5341854 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -87,6 +87,10 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }); const [isSubmitted, setIsSubmitted] = useState(false); + // ===== DATE ERROR STATE ===== + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); + const filterModal = useModal(); const { @@ -106,6 +110,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { const handleFilterModalOpen = () => { filterModal.openModal(); + formik.validateForm(); }; // ===== FORMIK SETUP ===== @@ -349,6 +354,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }; }, [tabId, clearTabActions]); + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + }, [filterModal.open, dateErrorShown]); + const getTableColumns = (supplier?: DebtSupplier): ColumnDef[] => [ { id: 'no', @@ -723,7 +745,31 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { name='startDate' value={formik.values.startDate || ''} onChange={(e) => { - formik.setFieldValue('startDate', e.target.value || null); + const value = e.target.value; + formik.setFieldValue('startDate', value || null); + + if (value && formik.values.endDate) { + const startDate = new Date(value); + const endDateObj = new Date(formik.values.endDate); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } }} className={{ wrapper: 'w-full' }} isError={ @@ -737,10 +783,36 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { name='endDate' value={formik.values.endDate || ''} onChange={(e) => { - formik.setFieldValue('endDate', e.target.value || null); + const value = e.target.value; + formik.setFieldValue('endDate', value || null); + + if (value && formik.values.startDate) { + const startDateObj = new Date(formik.values.startDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }} className={{ wrapper: 'w-full' }} - isError={formik.touched.endDate && !!formik.errors.endDate} + isError={ + (formik.touched.endDate && !!formik.errors.endDate) || + hasDateError + } errorMessage={formik.errors.endDate} isNestedModal /> diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 6023a123..d0fb8d47 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -842,18 +842,25 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 219a380d..0f68d3b0 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -80,6 +80,10 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { // ===== FILTER STATE ===== const [filterParams, setFilterParams] = useState({}); + // ===== DATE ERROR STATE ===== + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); + const filterModal = useModal(); // ===== OPTIONS ===== @@ -448,6 +452,23 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }; }, [tabId, clearTabActions]); + useEffectHook(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + useEffectHook(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + }, [filterModal.open, dateErrorShown]); + const getTableColumns = (): ColumnDef[] => { const tableColumns: ColumnDef[] = [ { @@ -791,34 +812,76 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { placeholder='Pilih Tanggal Awal' value={formik.values.start_date || ''} onChange={(e) => { - formik.setFieldValue('start_date', e.target.value || null); + const value = e.target.value; + formik.setFieldValue('start_date', value || null); + + if (value && formik.values.end_date) { + const startDate = new Date(value); + const endDateObj = new Date(formik.values.end_date); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } }} className={{ wrapper: 'w-full' }} + errorMessage={formik.errors.start_date} isError={ !!formik.errors.start_date && formik.touched.start_date } /> - {formik.errors.start_date && formik.touched.start_date && ( -
- {formik.errors.start_date} -
- )} { - formik.setFieldValue('end_date', e.target.value || null); + const value = e.target.value; + formik.setFieldValue('end_date', value || null); + + if (value && formik.values.start_date) { + const startDateObj = new Date(formik.values.start_date); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }} className={{ wrapper: 'w-full' }} - isError={!!formik.errors.end_date && formik.touched.end_date} + errorMessage={formik.errors.end_date} + isError={ + (formik.errors.end_date && formik.touched.end_date) || + hasDateError + } /> - {formik.errors.end_date && formik.touched.end_date && ( -
- {formik.errors.end_date} -
- )}
From a89e83af29ba3ad9064c156f27ebd6b3eb2e0cfe Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 15:57:25 +0700 Subject: [PATCH 032/149] refactor(FE): Fix multiline formatting for isError conditions --- src/components/pages/finance/FinanceTable.tsx | 7 +++++-- .../transfer-to-laying/TransferToLayingFilterModal.tsx | 3 ++- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 7 +++++-- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 7 +++++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 10d959b9..b30308a5 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -661,7 +661,8 @@ const FinanceTable = () => { errorMessage={filterFormik.errors.start_date} onChange={startDateChangeHandler} isError={ - filterFormik.touched.start_date && Boolean(filterFormik.errors.start_date) + filterFormik.touched.start_date && + Boolean(filterFormik.errors.start_date) } /> { errorMessage={filterFormik.errors.end_date} onChange={endDateChangeHandler} isError={ - (filterFormik.touched.end_date && Boolean(filterFormik.errors.end_date)) || hasDateError + (filterFormik.touched.end_date && + Boolean(filterFormik.errors.end_date)) || + hasDateError } />
diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 4b596155..2e4051ce 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -849,7 +849,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { className={{ wrapper: 'w-full' }} isNestedModal isError={ - formik.touched.start_date && Boolean(formik.errors.start_date) + formik.touched.start_date && + Boolean(formik.errors.start_date) } />
@@ -862,7 +863,9 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { className={{ wrapper: 'w-full' }} isNestedModal isError={ - (formik.touched.end_date && Boolean(formik.errors.end_date)) || hasDateError + (formik.touched.end_date && + Boolean(formik.errors.end_date)) || + hasDateError } />
diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index d0fb8d47..5eaa67e1 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -847,7 +847,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { className={{ wrapper: 'w-full' }} isNestedModal isError={ - formik.touched.start_date && Boolean(formik.errors.start_date) + formik.touched.start_date && + Boolean(formik.errors.start_date) } />
@@ -859,7 +860,9 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { className={{ wrapper: 'w-full' }} isNestedModal isError={ - (formik.touched.end_date && Boolean(formik.errors.end_date)) || hasDateError + (formik.touched.end_date && + Boolean(formik.errors.end_date)) || + hasDateError } />
From 0031a65f97c2d61f7134623e70d1c43e2fc30b57 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 25 Feb 2026 16:30:31 +0700 Subject: [PATCH 033/149] refactor(FE): Ensure filter modal closes on reset across components --- src/components/pages/closing/ClosingsTable.tsx | 1 + src/components/pages/production/uniformity/UniformityTable.tsx | 1 + src/components/pages/report/expense/tab/ReportExpenseTab.tsx | 3 ++- src/components/pages/report/finance/tab/CustomerPaymentTab.tsx | 1 + src/components/pages/report/finance/tab/DebtSupplierTab.tsx | 1 + .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 1 + .../pages/report/marketing/tab/DailyMarketingTab.tsx | 1 + src/components/pages/report/marketing/tab/HppPerKandangTab.tsx | 1 + .../tab/ProductionResultProjectFlockKandangTab.tsx | 1 + 9 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index fc5984b5..7885c75c 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -159,6 +159,7 @@ const ClosingsTable = () => { onReset: () => { updateFilter('location_id', ''); updateFilter('project_status', ''); + filterModal.closeModal(); }, }); diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 615e4941..c420ec6c 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -493,6 +493,7 @@ const UniformityTable = () => { updateFilter('kandang_id', ''); filterFormik.resetForm(); + filterModal.closeModal(); }, [filterFormik, updateFilter]); const selectedRowIds = useMemo(() => { diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index b9296f89..971536c5 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -15,7 +15,7 @@ import { import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; import Table from '@/components/Table'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { formatCurrency, formatDate } from '@/lib/helper'; import { ReportExpense } from '@/types/api/report/report-expense'; import { ReportExpenseApi } from '@/services/api/report'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -145,6 +145,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { setFilterParams({}); setIsSubmitted(false); setPage(1); + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 2e4051ce..26577109 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -119,6 +119,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { toast.dismiss(); setDateErrorShown(false); } + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index d5341854..fb02e959 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -142,6 +142,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { filter_by: undefined, }); setIsSubmitted(false); + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 5eaa67e1..afcae9a4 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -147,6 +147,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { toast.dismiss(); setDateErrorShown(false); } + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 0f68d3b0..0be358d8 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -142,6 +142,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { onReset: () => { setFilterParams({}); setIsSubmitted(false); + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 953b4a88..80d9da2e 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -137,6 +137,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { toast.dismiss(); setDateErrorShown(false); } + filterModal.closeModal(); }, }); diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index 20dddff1..9e844ad3 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -238,6 +238,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { setFilterParams({}); setIsSubmitted(false); setPage(1); + filterModal.closeModal(); }, }); From 5a679017227baeb73a939478490298ce12da01fb Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 26 Feb 2026 09:11:05 +0700 Subject: [PATCH 034/149] feat(FE): Add transaction type and subtype options to constants --- src/config/constant.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index 66d0af7d..9db7b6cd 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -545,3 +545,20 @@ export const MARKETING_DATE_FILTER_TYPE_OPTIONS = [ value: 'so_date', }, ]; + +export const TRANSACTION_TYPE_OPTIONS = [ + { label: 'Pembelian', value: 'PEMBELIAN' }, + { label: 'Penjualan', value: 'PENJUALAN' }, + { label: 'Biaya', value: 'BIAYA' }, +]; + +export const TRANSACTION_SUBTYPE_OPTIONS = { + PEMBELIAN: [{ label: 'Pembelian', value: 'PURCHASE_IN' }], + PENJUALAN: [{ label: 'Penjualan', value: 'MARKETING_OUT' }], + RECORDING: [ + { label: 'Recording Stock Out', value: 'RECORDING_STOCK_OUT' }, + { label: 'Recording Depletion Out', value: 'RECORDING_DEPLETION_OUT' }, + { label: 'Recording Depletion In', value: 'RECORDING_DEPLETION_IN' }, + { label: 'Recording Egg In', value: 'RECORDING_EGG_IN' }, + ], +}; From 88b9c890e510219d3fc52c2f7d8ff97fbf6c0883 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 26 Feb 2026 10:10:59 +0700 Subject: [PATCH 035/149] refactor(FE): Refactor inventory adjustment form --- .../form/InventoryAdjustmentForm.schema.ts | 145 ++- .../form/InventoryAdjustmentForm.tsx | 896 ++++++++++++++---- src/config/constant.ts | 6 +- src/types/api/inventory/adjustment.d.ts | 34 +- 4 files changed, 823 insertions(+), 258 deletions(-) diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts index 42ecf48d..8858cf25 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts @@ -1,55 +1,102 @@ import * as Yup from 'yup'; -import { OptionType } from '@/components/input/SelectInput'; -export const InventoryAdjustmentFormSchema = Yup.object({ - product_category: Yup.mixed() - .nullable() - .test( - 'is-valid-option', - 'Kategori Produk wajib diisi!', - (value) => value !== null && value !== undefined - ), +export type InventoryAdjustmentFormSchemaType = { + location: { + value: number; + label: string; + } | null; + location_id: number; + project_flock: { + value: number; + label: string; + } | null; + project_flock_id: number; + kandang: { + value: number; + label: string; + } | null; + kandang_id: number; + project_flock_kandang: { + value: number; + label: string; + } | null; + project_flock_kandang_id: number; + product: { + value: number; + label: string; + } | null; + product_id: number; + transaction_type: string; + transaction_subtype: string; + qty: number | string; + price: number | string; + notes: string; +}; - product_category_id: Yup.number().nullable(), - - product: Yup.mixed() - .nullable() - .test( - 'is-valid-option', - 'Produk wajib diisi!', - (value) => value !== null && value !== undefined - ), - - product_id: Yup.number() - .nullable() - .required('Produk wajib diisi!') - .min(1, 'Produk wajib diisi!'), - - warehouse: Yup.mixed() - .nullable() - .test( - 'is-valid-option', - 'Warehouse wajib diisi!', - (value) => value !== null && value !== undefined - ), - - warehouse_id: Yup.number() - .nullable() - .required('Warehouse wajib diisi!') - .min(1, 'Warehouse wajib diisi!'), - - transaction_type: Yup.string() - .oneOf(['increase', 'decrease'], 'Tipe transaksi tidak valid') - .nullable() - .required('Tipe transaksi wajib diisi'), - - quantity: Yup.number() - .typeError('Kuantitas harus berupa angka') - .min(1, 'Minimal kuantitas adalah 1') - .required('Kuantitas wajib diisi'), - - note: Yup.string().required('Catatan wajib diisi!'), -}); +export const InventoryAdjustmentFormSchema: Yup.ObjectSchema = + Yup.object({ + location: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + location_id: Yup.number() + .min(1, 'Lokasi wajib diisi!') + .required('Lokasi wajib diisi!') + .typeError('Lokasi wajib diisi!'), + project_flock: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_id: Yup.number() + .min(1, 'Project flock wajib diisi!') + .required('Project flock wajib diisi!') + .typeError('Project flock wajib diisi!'), + kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + kandang_id: Yup.number() + .min(1, 'Kandang wajib diisi!') + .required('Kandang wajib diisi!') + .typeError('Kandang wajib diisi!'), + project_flock_kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_kandang_id: Yup.number() + .default(0) + .typeError('Project Flock Kandang wajib diisi!') + .test( + 'is-valid-project-flock-kandang', + 'Project Flock Kandang wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Project Flock Kandang wajib diisi!'), + product: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + product_id: Yup.number() + .min(1, 'Produk wajib diisi!') + .required('Produk wajib diisi!') + .typeError('Produk wajib diisi!'), + transaction_type: Yup.string() + .oneOf( + ['PEMBELIAN', 'PENJUALAN', 'BIAYA', 'RECORDING'], + 'Tipe transaksi tidak valid' + ) + .required('Tipe transaksi wajib diisi'), + transaction_subtype: Yup.string().required('Sub tipe transaksi wajib diisi'), + qty: Yup.number() + .typeError('Kuantitas harus berupa angka') + .min(1, 'Minimal kuantitas adalah 1') + .required('Kuantitas wajib diisi'), + price: Yup.number() + .typeError('Harga harus berupa angka') + .min(0, 'Minimal harga adalah 0') + .required('Harga wajib diisi'), + notes: Yup.string().required('Catatan wajib diisi!'), + }); export type InventoryAdjustmentFormValues = Yup.InferType< typeof InventoryAdjustmentFormSchema diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index 612fbb20..31b64bed 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { isResponseError } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { CreateInventoryAdjustmentPayload, @@ -14,11 +14,11 @@ import { InventoryAdjustmentFormSchema, InventoryAdjustmentFormValues, } from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema'; +import { LocationApi, ProductApi } from '@/services/api/master-data'; import { - ProductApi, - ProductCategoryApi, - WarehouseApi, -} from '@/services/api/master-data'; + ProjectFlockApi, + ProjectFlockKandangApi, +} from '@/services/api/production'; import Button from '@/components/Button'; import { Icon } from '@iconify/react'; import SelectInput, { @@ -26,13 +26,22 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import TextInput from '@/components/input/TextInput'; -import { RadioGroup } from '@/components/input/RadioInput'; import TextArea from '@/components/input/TextArea'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; -import { ProductCategory } from '@/types/api/master-data/product-category'; +import { Location } from '@/types/api/master-data/location'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { Kandang } from '@/types/api/master-data/kandang'; import { Product } from '@/types/api/master-data/product'; -import { Warehouse } from '@/types/api/master-data/warehouse'; +import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; +import { BaseApiResponse } from '@/types/api/api-general'; +import useSWR from 'swr'; +import { + TRANSACTION_TYPE_OPTIONS, + TRANSACTION_SUBTYPE_OPTIONS, +} from '@/config/constant'; +import NumberInput from '@/components/input/NumberInput'; interface InventoryAdjustmentFormProps { type?: 'add' | 'edit' | 'detail'; @@ -49,8 +58,19 @@ const InventoryAdjustmentForm = ({ InventoryAdjustmentFormErrorMessage, setInventoryAdjustmentFormErrorMessage, ] = useState(''); - const [disabledProduct, setDisabledProduct] = useState(true); - const [quantityLabel, setQuantityLabel] = useState('Tambah Stok'); + const [quantityLabel, setQuantityLabel] = useState('Kuantitas'); + + // Selected States untuk cascading selects + const [selectedLocation, setSelectedLocation] = useState( + null + ); + const [selectedProjectFlock, setSelectedProjectFlock] = + useState(null); + const [selectedKandang, setSelectedKandang] = useState( + null + ); + const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] = + useState(''); // Submit Handler const createInventoryAdjustmentHandler = useCallback( @@ -71,34 +91,258 @@ const InventoryAdjustmentForm = ({ [router] ); - const formikInitialValues = useMemo< - Partial - >(() => { - return { - product_id: initialValues?.product_warehouse?.product_id ?? 0, - warehouse_id: initialValues?.product_warehouse?.warehouse_id ?? 0, - product_category: undefined, - product: undefined, - warehouse: undefined, - quantity: initialValues?.increase ?? initialValues?.decrease ?? 0, - transaction_type: undefined, - note: initialValues?.note ?? '', - }; - }, [initialValues]); + // API Data Fetching + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setProjectFlockInputValue, + options: projectFlockOptions, + rawData: projectFlocksRawData, + isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + location_id: selectedProjectFlockLocationId, + } + ); + + // Lookup URL untuk mendapatkan project_flock_kandang_id + const projectFlockKandangLookupUrl = useMemo(() => { + if (!selectedProjectFlock || !selectedKandang) return null; + const params = new URLSearchParams({ + project_flock_id: selectedProjectFlock.value.toString(), + kandang_id: selectedKandang.value.toString(), + }); + return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; + }, [selectedProjectFlock, selectedKandang]); + + const { data: projectFlockKandangLookupData } = useSWR( + projectFlockKandangLookupUrl, + projectFlockKandangLookupUrl + ? () => + ProjectFlockApi.getAllFetcher( + projectFlockKandangLookupUrl + ) as Promise> + : null + ); + + const projectFlockKandangLookup = + projectFlockKandangLookupData?.status === 'success' + ? projectFlockKandangLookupData.data + : undefined; + + // Fetch project_flock_kandang detail untuk edit mode saja (tidak perlu untuk detail) + const projectFlockKandangDetailUrl = useMemo(() => { + if (type !== 'edit' || !initialValues?.project_flock_kandang_id) + return null; + return `${ProjectFlockKandangApi.basePath}/${initialValues.project_flock_kandang_id}`; + }, [type, initialValues?.project_flock_kandang_id]); + + const { data: projectFlockKandangDetailData } = useSWR( + projectFlockKandangDetailUrl, + projectFlockKandangDetailUrl + ? () => + ProjectFlockKandangApi.getAllFetcher( + projectFlockKandangDetailUrl + ) as Promise> + : null + ); + + const projectFlockKandangDetail = + projectFlockKandangDetailData?.status === 'success' + ? projectFlockKandangDetailData.data + : undefined; + + // Fetch approved project flock kandangs untuk filter kandang options + const approvedProjectFlockKandangsUrl = useMemo(() => { + const params = new URLSearchParams({ + step_name: 'Disetujui', + limit: '100', + }); + return `${ProjectFlockKandangApi.basePath}?${params.toString()}`; + }, []); + + const { data: approvedProjectFlockKandangsData } = useSWR( + approvedProjectFlockKandangsUrl, + ProjectFlockKandangApi.getAllFetcher + ); + + const approvedProjectFlockKandangs = useMemo(() => { + if (!isResponseSuccess(approvedProjectFlockKandangsData)) return []; + return approvedProjectFlockKandangsData.data; + }, [approvedProjectFlockKandangsData]); + + // Product select dengan filter project_flock_kandang_id - hanya fetch jika project_flock_kandang_id ada + const productUrl = useMemo(() => { + if (!projectFlockKandangLookup?.project_flock_kandang_id) return null; + const params = new URLSearchParams({ + project_flock_kandang_id: + projectFlockKandangLookup.project_flock_kandang_id.toString(), + page: '1', + limit: '10', + }); + return `${ProductApi.basePath}?${params.toString()}`; + }, [projectFlockKandangLookup?.project_flock_kandang_id]); + + const { data: productData, isLoading: isLoadingProductOptions } = useSWR( + productUrl, + productUrl ? ProductApi.getAllFetcher : null + ); + + const productOptions = useMemo(() => { + if (!isResponseSuccess(productData)) return []; + return productData.data.map((p: Product) => ({ + value: p.id, + label: p.name, + })); + }, [productData]); + + const setProductInputValue = useCallback((value: string) => { + // Implementasi search jika diperlukan + }, []); + + const loadMoreProducts = useCallback(() => { + // Implementasi load more jika diperlukan + }, []); + + // Kandang options dari project flock data (filtered by approved status untuk add mode) + const kandangOptions = useMemo(() => { + let options: OptionType[] = []; + + if (selectedProjectFlock && isResponseSuccess(projectFlocksRawData)) { + const data = projectFlocksRawData.data as ProjectFlock[]; + const selectedProjectFlockData = data.find( + (pf) => pf.id === selectedProjectFlock.value + ); + + if (selectedProjectFlockData?.kandangs) { + // Get approved kandang ids untuk project flock yang dipilih + const approvedKandangIds = approvedProjectFlockKandangs + .filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value) + .map((pfk) => pfk.kandang_id); + + const kandangOptions = selectedProjectFlockData.kandangs + .filter((kandang: Kandang) => { + // Untuk add mode, hanya tampilkan kandang yang approved + if (type === 'add') { + return approvedKandangIds.includes(kandang.id); + } + return true; + }) + .map((kandang: Kandang) => ({ + value: kandang.id, + label: kandang.name || '', + })); + options = options.concat(kandangOptions); + } + } + + if (projectFlockKandangDetail && type === 'edit') { + const currentKandang = projectFlockKandangDetail.kandang; + if ( + currentKandang && + !options.find((opt) => opt.value === currentKandang.id) + ) { + options.push({ + value: currentKandang.id, + label: currentKandang.name || '', + }); + } + } + + return options; + }, [ + selectedProjectFlock, + projectFlocksRawData, + projectFlockKandangDetail, + type, + approvedProjectFlockKandangs, + ]); + + // Enhanced options untuk edit/detail + const enhancedLocationOptions = useMemo(() => { + const options = [...locationOptions]; + + if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { + const currentLocation = projectFlockKandangDetail.project_flock.location; + if ( + currentLocation && + !options.find((opt) => opt.value === currentLocation.id) + ) { + options.push({ + value: currentLocation.id, + label: currentLocation.name || '', + }); + } + } + + return options; + }, [locationOptions, projectFlockKandangDetail, type]); + + const enhancedProjectFlockOptions = useMemo(() => { + const options = [...projectFlockOptions]; + + if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { + const currentProjectFlock = projectFlockKandangDetail.project_flock; + if ( + currentProjectFlock && + !options.find((opt) => opt.value === currentProjectFlock.id) + ) { + options.push({ + value: currentProjectFlock.id, + label: currentProjectFlock.flock_name || '', + }); + } + } + + return options; + }, [projectFlockOptions, projectFlockKandangDetail, type]); + + // Formik Initial Values + const formikInitialValues = useMemo>( + () => ({ + location: null, + location_id: 0, + project_flock: null, + project_flock_id: 0, + kandang: null, + kandang_id: 0, + project_flock_kandang: null, + project_flock_kandang_id: 0, + product: null, + product_id: 0, + transaction_type: '', + transaction_subtype: '', + qty: '', + price: '', + notes: '', + }), + [] + ); // Formik const formik = useFormik({ - enableReinitialize: true, + enableReinitialize: false, initialValues: formikInitialValues as InventoryAdjustmentFormValues, validationSchema: InventoryAdjustmentFormSchema, onSubmit: async (values) => { setInventoryAdjustmentFormErrorMessage(''); const payload: CreateInventoryAdjustmentPayload = { - product_id: values.product_id as number, - warehouse_id: values.warehouse_id as number, - quantity: values.quantity as number, - transaction_type: values.transaction_type as string, - note: values.note, + project_flock_kandang_id: values.project_flock_kandang_id, + product_id: values.product_id, + transaction_subtype: values.transaction_subtype, + qty: Number(values.qty), + price: Number(values.price), + notes: values.notes, }; switch (type) { @@ -109,111 +353,318 @@ const InventoryAdjustmentForm = ({ }, }); - // Fetch Data - const { - setInputValue: setProductCategoryInputValue, - options: productCategoryOptions, - isLoadingOptions: isLoadingProductCategoryOptions, - loadMore: loadMoreProductCategories, - } = useSelect(ProductCategoryApi.basePath, 'id', 'name'); + // Transaction subtype options berdasarkan transaction_type + const transactionSubtypeOptions = useMemo(() => { + const transactionType = formik.values.transaction_type; - const { - setInputValue: setProductInputValue, - options: productOptions, - isLoadingOptions: isLoadingProductOptions, - loadMore: loadMoreProducts, - } = useSelect(ProductApi.basePath, 'id', 'name', 'search', { - product_category_id: formik.values.product_category_id - ? String(formik.values.product_category_id) - : '', - }); + if (transactionType === 'RECORDING') { + return TRANSACTION_SUBTYPE_OPTIONS.RECORDING; + } - const { - setInputValue: setWarehouseInputValue, - options: warehouseOptions, - isLoadingOptions: isLoadingWarehouseOptions, - loadMore: loadMoreWarehouses, - } = useSelect(WarehouseApi.basePath, 'id', 'name'); + return []; + }, [formik.values.transaction_type]); - // Options Handler - const productCategoryChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - formik.setFieldTouched('product_category_id', true); - formik.setFieldValue('product_category_id', (val as OptionType)?.value); + // Cek apakah subtype readonly (untuk PEMBELIAN/PENJUALAN) + const isTransactionSubtypeReadonly = useMemo(() => { + const transactionType = formik.values.transaction_type; + return transactionType === 'PEMBELIAN' || transactionType === 'PENJUALAN'; + }, [formik.values.transaction_type]); - formik.setFieldValue('product_category', val); + // Update quantity label berdasarkan transaction_subtype + useEffect(() => { + const subtype = formik.values.transaction_subtype; + if ( + subtype === 'RECORDING_STOCK_OUT' || + subtype === 'RECORDING_DEPLETION_OUT' || + subtype === 'MARKETING_OUT' + ) { + setQuantityLabel('Kurangi Stok'); + } else if ( + subtype === 'RECORDING_STOCK_IN' || + subtype === 'RECORDING_DEPLETION_IN' || + subtype === 'RECORDING_EGG_IN' || + subtype === 'PURCHASE_IN' + ) { + setQuantityLabel('Tambah Stok'); + } else { + setQuantityLabel('Kuantitas'); + } + }, [formik.values.transaction_subtype]); - const disabled = (val as OptionType)?.value == null; - setDisabledProduct(disabled); - formik.setFieldValue('product_id', 0); + // Event Handlers + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + const location = val as OptionType | null; + const locationId = location ? Number(location.value) : 0; + + formik.setFieldTouched('location', true); + formik.setFieldValue('location', location); + formik.setFieldTouched('location_id', true); + formik.setFieldValue('location_id', locationId); + + setSelectedLocation(location); + setSelectedProjectFlockLocationId( + location ? location.value.toString() : '' + ); + + // Reset dependent fields + setSelectedProjectFlock(null); + setSelectedKandang(null); + formik.setFieldValue('project_flock', null); + formik.setFieldValue('project_flock_id', 0); + formik.setFieldValue('kandang', null); + formik.setFieldValue('kandang_id', 0); + formik.setFieldValue('project_flock_kandang', null); + formik.setFieldValue('project_flock_kandang_id', 0); formik.setFieldValue('product', null); - formik.setFieldTouched('product', false); - formik.setFieldTouched('product_id', false); + formik.setFieldValue('product_id', 0); + }; + + const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { + const projectFlock = val as OptionType | null; + const projectFlockId = Number(projectFlock?.value); + + formik.setFieldTouched('project_flock', true); + formik.setFieldValue('project_flock', projectFlock); + formik.setFieldTouched('project_flock_id', true); + formik.setFieldValue('project_flock_id', projectFlockId); + + setSelectedProjectFlock(projectFlock); + setSelectedKandang(null); + + // Reset dependent fields + formik.setFieldValue('kandang', null); + formik.setFieldValue('kandang_id', 0); + formik.setFieldValue('project_flock_kandang', null); + formik.setFieldValue('project_flock_kandang_id', 0); + formik.setFieldValue('product', null); + formik.setFieldValue('product_id', 0); + }; + + const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { + const kandang = val as OptionType | null; + const kandangId = Number(kandang?.value); + + formik.setFieldTouched('kandang', true); + formik.setFieldValue('kandang', kandang); + formik.setFieldTouched('kandang_id', true); + formik.setFieldValue('kandang_id', kandangId); + + setSelectedKandang(kandang); + + // Reset product karena kandang berubah + formik.setFieldValue('product', null); + formik.setFieldValue('product_id', 0); + formik.setFieldTouched('project_flock_kandang', true); + formik.setFieldTouched('project_flock_kandang_id', true); }; const productChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('product', true); formik.setFieldValue('product', val); - formik.setFieldTouched('product_id', true); - formik.setFieldValue('product_id', (val as OptionType)?.value); + formik.setFieldValue('product_id', (val as OptionType)?.value ?? 0); }; - const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('warehouse', val); + const transactionTypeChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + const selectedType = (val as OptionType)?.value as string; - formik.setFieldTouched('warehouse_id', true); - formik.setFieldValue('warehouse_id', (val as OptionType)?.value); + formik.setFieldTouched('transaction_type', true); + formik.setFieldValue('transaction_type', selectedType); + + // Reset transaction_subtype + formik.setFieldValue('transaction_subtype', ''); + + // Auto-fill transaction_subtype untuk PEMBELIAN dan PENJUALAN + if (selectedType === 'PEMBELIAN') { + formik.setFieldValue( + 'transaction_subtype', + TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.value + ); + } else if (selectedType === 'PENJUALAN') { + formik.setFieldValue( + 'transaction_subtype', + TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.value + ); + } + }; + + const transactionSubtypeChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + const selectedSubtype = (val as OptionType)?.value as string; + formik.setFieldTouched('transaction_subtype', true); + formik.setFieldValue('transaction_subtype', selectedSubtype); }; const resetHandler = () => { formik.resetForm(); - setQuantityLabel('Tambah Stok'); - productCategoryChangeHandler(null); - productChangeHandler(null); - warehouseChangeHandler(null); + setQuantityLabel('Kuantitas'); + setSelectedLocation(null); + setSelectedProjectFlock(null); + setSelectedKandang(null); + setSelectedProjectFlockLocationId(''); }; - const { setValues: formikSetValues } = formik; - - // Effect + // Effect - Set project_flock_kandang_id dari lookup useEffect(() => { - if (initialValues?.product_warehouse?.product?.id) { - setDisabledProduct(false); - formik.setFieldValue( - 'product_id', - initialValues.product_warehouse.product.id - ); - formik.setFieldValue('product', { - value: initialValues.product_warehouse.product.id, - label: initialValues.product_warehouse.product.name, - }); - formik.setFieldValue( - 'warehouse_id', - initialValues.product_warehouse.warehouse.id - ); - formik.setFieldValue('warehouse', { - value: initialValues.product_warehouse.warehouse.id, - label: initialValues.product_warehouse.warehouse.name, - }); - formik.setFieldValue( - 'quantity', - initialValues.product_warehouse.quantity - ); - formik.setFieldValue('note', initialValues.note); + if (projectFlockKandangLookup?.project_flock_kandang_id) { + const projectFlockKandangId = + projectFlockKandangLookup.project_flock_kandang_id; + + if (formik.values.project_flock_kandang_id !== projectFlockKandangId) { + formik.setFieldValue('project_flock_kandang_id', projectFlockKandangId); + formik.setFieldValue('project_flock_kandang', { + value: projectFlockKandangId, + label: `${projectFlockKandangLookup.project_flock.flock_name} - ${projectFlockKandangLookup.kandang.name}`, + }); + } } - }, [formik, initialValues, setQuantityLabel, setDisabledProduct]); - useEffect(() => { - formikSetValues(formikInitialValues as InventoryAdjustmentFormValues); - }, [formikSetValues, formikInitialValues]); + }, [projectFlockKandangLookup, formik.values.project_flock_kandang_id]); - // Utils Function - const formatNumber = (value: string) => { - const numericValue = value.replace(/[^0-9.]/g, ''); - const [integer, decimal] = numericValue.split('.'); - const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ','); - return decimal ? `${formattedInteger}.${decimal}` : formattedInteger; - }; + // Effect - Set initial values untuk edit mode (dengan projectFlockKandangDetail) + useEffect(() => { + if (initialValues && type === 'edit') { + const transactionSubtype = initialValues.transaction_subtype; + + // Determine transaction_type dari transaction_subtype + let transactionType = ''; + if (transactionSubtype === 'PURCHASE_IN') { + transactionType = 'PEMBELIAN'; + } else if (transactionSubtype === 'MARKETING_OUT') { + transactionType = 'PENJUALAN'; + } else if (transactionSubtype?.startsWith('RECORDING')) { + transactionType = 'RECORDING'; + } + + // Set lokasi + if (initialValues.location) { + const locationOption = { + value: initialValues.location.id, + label: initialValues.location.name, + }; + setSelectedLocation(locationOption); + setSelectedProjectFlockLocationId(initialValues.location.id.toString()); + } + + // Set project flock + if (initialValues.project_flock) { + const projectFlockOption = { + value: initialValues.project_flock.id, + label: initialValues.project_flock.flock_name, + }; + setSelectedProjectFlock(projectFlockOption); + } + + // Set kandang dari project_flock_kandang jika ada (hanya untuk edit mode) + if (projectFlockKandangDetail) { + const kandangOption = { + value: projectFlockKandangDetail.kandang.id, + label: projectFlockKandangDetail.kandang.name || '', + }; + setSelectedKandang(kandangOption); + } + + formik.setValues({ + location: initialValues.location + ? { + value: initialValues.location.id, + label: initialValues.location.name, + } + : null, + location_id: initialValues.location?.id ?? 0, + project_flock: initialValues.project_flock + ? { + value: initialValues.project_flock.id, + label: initialValues.project_flock.flock_name, + } + : null, + project_flock_id: initialValues.project_flock?.id ?? 0, + kandang: projectFlockKandangDetail?.kandang + ? { + value: projectFlockKandangDetail.kandang.id, + label: projectFlockKandangDetail.kandang.name || '', + } + : null, + kandang_id: projectFlockKandangDetail?.kandang?.id ?? 0, + project_flock_kandang: initialValues.project_flock_kandang_id + ? { + value: initialValues.project_flock_kandang_id, + label: `${initialValues.project_flock?.flock_name || ''} - ${projectFlockKandangDetail?.kandang?.name || ''}`, + } + : null, + project_flock_kandang_id: initialValues.project_flock_kandang_id ?? 0, + product: initialValues.product_warehouse?.product + ? { + value: initialValues.product_warehouse.product.id, + label: initialValues.product_warehouse.product.name, + } + : null, + product_id: initialValues.product_warehouse?.product?.id ?? 0, + transaction_type: transactionType, + transaction_subtype: transactionSubtype, + qty: initialValues.qty ?? '', + price: initialValues.price ?? '', + notes: initialValues.notes ?? '', + }); + } + }, [formik.setValues, initialValues, projectFlockKandangDetail, type]); + + // Effect - Set initial values untuk detail mode (tanpa projectFlockKandangDetail) + useEffect(() => { + if (initialValues && type === 'detail') { + const transactionSubtype = initialValues.transaction_subtype; + + // Determine transaction_type dari transaction_subtype + let transactionType = ''; + if (transactionSubtype === 'PURCHASE_IN') { + transactionType = 'PEMBELIAN'; + } else if (transactionSubtype === 'MARKETING_OUT') { + transactionType = 'PENJUALAN'; + } else if (transactionSubtype?.startsWith('RECORDING')) { + transactionType = 'RECORDING'; + } + + formik.setValues({ + location: initialValues.location + ? { + value: initialValues.location.id, + label: initialValues.location.name, + } + : null, + location_id: initialValues.location?.id ?? 0, + project_flock: initialValues.project_flock + ? { + value: initialValues.project_flock.id, + label: initialValues.project_flock.flock_name, + } + : null, + project_flock_id: initialValues.project_flock?.id ?? 0, + kandang: null, // Tidak perlu kandang untuk detail mode + kandang_id: 0, + project_flock_kandang: initialValues.project_flock_kandang_id + ? { + value: initialValues.project_flock_kandang_id, + label: `${initialValues.project_flock?.flock_name || ''} - Kandang`, + } + : null, + project_flock_kandang_id: initialValues.project_flock_kandang_id ?? 0, + product: initialValues.product_warehouse?.product + ? { + value: initialValues.product_warehouse.product.id, + label: initialValues.product_warehouse.product.name, + } + : null, + product_id: initialValues.product_warehouse?.product?.id ?? 0, + transaction_type: transactionType, + transaction_subtype: transactionSubtype, + qty: initialValues.qty ?? '', + price: initialValues.price ?? '', + notes: initialValues.notes ?? '', + }); + } + }, [formik.setValues, initialValues, type]); // ===== Formik Error List ===== const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); @@ -244,25 +695,60 @@ const InventoryAdjustmentForm = ({ className='w-full mt-8 flex flex-col gap-6' >
- {/* Select Input Product Category */} + {/* Select Input Location */} + {/* Select Input Project Flock */} + + + {/* Select Input Kandang */} + + {/* Select Input Product */} - {/* Select Input Warehouse */} + {/* Select Input Transaction Type */} - - {/* Radio Button Flag Stock */} - { - formik.handleChange(e); - setQuantityLabel( - e.target.value === 'increase' ? 'Tambah Stok' : 'Kurangi Stok' - ); - }} - onBlur={formik.handleBlur} + value={ + formik.values.transaction_type + ? { + value: formik.values.transaction_type, + label: + TRANSACTION_TYPE_OPTIONS.find( + (opt) => opt.value === formik.values.transaction_type + )?.label || '', + } + : null + } + onChange={transactionTypeChangeHandler} + options={TRANSACTION_TYPE_OPTIONS} isError={ formik.touched.transaction_type && Boolean(formik.errors.transaction_type) } errorMessage={formik.errors.transaction_type as string} - color='primary' - required - bottomLabel={ - formik.values.transaction_type == undefined - ? 'Pilih salah satu tipe transaksi' - : undefined - } - disabled={type === 'detail'} + isDisabled={type === 'detail'} + placeholder='Pilih Tipe Transaksi' + isClearable /> - {/* Number Input Stock */} - + opt.value === + formik.values.transaction_subtype + )?.label || '', + } + : null + } + onChange={transactionSubtypeChangeHandler} + options={transactionSubtypeOptions} + isError={ + formik.touched.transaction_subtype && + Boolean(formik.errors.transaction_subtype) + } + errorMessage={formik.errors.transaction_subtype as string} + isDisabled={ + type === 'detail' || + isTransactionSubtypeReadonly || + formik.values.transaction_type === '' + } + placeholder={ + formik.values.transaction_type === '' + ? 'Pilih Tipe Transaksi terlebih dahulu' + : isTransactionSubtypeReadonly + ? 'Otomatis terisi' + : 'Pilih Sub Tipe Transaksi' + } + isClearable + /> + + {/* Number Input Quantity */} + { - const rawValue = e.target.value.replace(/,/g, ''); - const numericValue = parseFloat(rawValue); - if (!isNaN(numericValue)) { - formik.setFieldValue('quantity', numericValue); - } else { - formik.setFieldValue('quantity', 0); - } - }} + name='qty' + value={formik.values.qty} + onChange={formik.handleChange} onBlur={formik.handleBlur} - isError={ - formik.touched.quantity && Boolean(formik.errors.quantity) - } - errorMessage={formik.errors.quantity as string} + isError={formik.touched.qty && Boolean(formik.errors.qty)} + errorMessage={formik.errors.qty as string} readOnly={type === 'detail'} /> - {/* Text Area Input Reason */} -