refactor(FE): Refactor tab actions to use memoized components

This commit is contained in:
rstubryan
2026-03-05 11:59:27 +07:00
parent a7951b6c28
commit 2de6636bbf
3 changed files with 306 additions and 254 deletions
@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback, useRef } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
@@ -83,6 +83,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
const [dateErrorShown, setDateErrorShown] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false); const [hasDateError, setHasDateError] = useState(false);
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal(); const filterModal = useModal();
// ===== OPTIONS ===== // ===== OPTIONS =====
@@ -102,11 +104,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = const { options: customerOptions, isLoadingOptions: isLoadingCustomers } =
useSelect(CustomerApi.basePath, 'id', 'name', 'search'); useSelect(CustomerApi.basePath, 'id', 'name', 'search');
const handleFilterModalOpen = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== FORMIK SETUP ===== // ===== FORMIK SETUP =====
const formik = useFormik<DailyMarketingReportFilterType>({ const formik = useFormik<DailyMarketingReportFilterType>({
initialValues: { initialValues: {
@@ -353,121 +350,126 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
} }
}, [dailyMarketingsExport, summaryTotal]); }, [dailyMarketingsExport, summaryTotal]);
// ===== REGISTER TAB ACTIONS TO STORE ===== // ===== TAB ACTIONS COMPONENT =====
const setTabActions = useTabActionsStore((state) => state.setTabActions); const TabActions = useMemo(() => {
const clearTabActions = useTabActionsStore((state) => state.clearTabActions); return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffectHook(() => { useEffectHook(() => {
setTabActions( setTabActions(
tabId, tabId,
<div className='flex flex-row gap-3 items-center'> <div className='flex flex-row gap-3 items-center'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
placeholder='Search' placeholder='Search'
value={searchValue} value={searchValue}
onChange={searchChangeHandler} onChange={searchChangeHandler}
startAdornment={ startAdornment={
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
}
className={{
wrapper: 'w-full min-w-48 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input: 'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<ButtonFilter
values={formik.values}
fieldGroups={[['start_date', 'end_date']]}
onClick={handleFilterModalOpen}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon <Icon
icon='heroicons:cloud-arrow-down' icon='heroicons:magnifying-glass'
width={20} width={20}
height={20} height={20}
/> />
}
className={{
wrapper: 'w-full min-w-48 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<span>Export</span> <ButtonFilter
values={filterParams}
fieldGroups={[['start_date', 'end_date']]}
onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
className='px-3 py-2.5'
/>
<div className='w-px self-stretch bg-base-content/10' /> <Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<Icon icon='heroicons:chevron-down' width={14} height={14} /> <span>Export</span>
</div>
</Button> <div className='w-px self-stretch bg-base-content/10' />
}
> <Icon
<Button icon='heroicons:chevron-down'
variant='ghost' width={14}
color='none' height={14}
onClick={handleExportExcel} />
isLoading={isExcelExportLoading} </div>
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap' </Button>
> }
<Icon icon='heroicons:table-cells' width={20} height={20} /> >
Export to Excel <Button
</Button> variant='ghost'
<Button color='none'
variant='ghost' onClick={handleExportExcel}
color='none' isLoading={isExcelExportLoading}
onClick={handleExportPDF} className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
isLoading={isPdfExportLoading} >
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap' <Icon icon='heroicons:table-cells' width={20} height={20} />
> Export to Excel
<Icon icon='heroicons:document' width={20} height={20} /> </Button>
Export to PDF <Button
</Button> variant='ghost'
</Dropdown> color='none'
</div> onClick={handleExportPDF}
); isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
useEffectHook(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
return null;
};
}, [ }, [
tabId, tabId,
filterParams,
searchValue, searchValue,
formik.values,
isAnyExportLoading, isAnyExportLoading,
filterModal.open, handleExportExcel,
setTabActions, handleExportPDF,
isExcelExportLoading,
isPdfExportLoading,
searchChangeHandler,
]); ]);
useEffectHook(() => { const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
useEffectHook(() => {
return () => {
if (dateErrorShown) {
toast.dismiss();
}
};
}, [dateErrorShown]);
useEffectHook(() => {
return () => {
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
};
}, [filterModal.open, dateErrorShown]);
const getTableColumns = (): ColumnDef<DailyMarketingRow>[] => { const getTableColumns = (): ColumnDef<DailyMarketingRow>[] => {
const tableColumns: ColumnDef<DailyMarketingRow>[] = [ const tableColumns: ColumnDef<DailyMarketingRow>[] = [
@@ -639,6 +641,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
return ( return (
<> <>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'> <div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? ( {!isSubmitted ? (
<DailyMarketingReportSkeleton <DailyMarketingReportSkeleton
@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback, useRef } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
@@ -66,6 +66,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
// ===== FILTER STATE ===== // ===== FILTER STATE =====
const [filterParams, setFilterParams] = useState<FilterParams>({}); const [filterParams, setFilterParams] = useState<FilterParams>({});
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal(); const filterModal = useModal();
// ===== OPTIONS ===== // ===== OPTIONS =====
@@ -95,11 +97,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
[] []
); );
const handleFilterModalOpen = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== FORMIK SETUP ===== // ===== FORMIK SETUP =====
const formik = useFormik<HppPerKandangFilterType>({ const formik = useFormik<HppPerKandangFilterType>({
initialValues: { initialValues: {
@@ -140,6 +137,11 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
}, },
}); });
handleFilterModalOpenRef.current = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== WEIGHT CHANGE HANDLERS ===== // ===== WEIGHT CHANGE HANDLERS =====
const handleWeightMinChange = useCallback( const handleWeightMinChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -442,87 +444,103 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
allDocSuppliers, allDocSuppliers,
]); ]);
// ===== REGISTER TAB ACTIONS TO STORE ===== // ===== TAB ACTIONS COMPONENT =====
const setTabActions = useTabActionsStore((state) => state.setTabActions); const TabActions = useMemo(() => {
const clearTabActions = useTabActionsStore((state) => state.clearTabActions); return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffectHook(() => { useEffectHook(() => {
setTabActions( setTabActions(
tabId, tabId,
<div className='flex flex-row gap-3'> <div className='flex flex-row gap-3'>
<ButtonFilter <ButtonFilter
values={formik.values} values={filterParams}
onClick={handleFilterModalOpen} onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline' variant='outline'
color='none' className='px-3 py-2.5'
isLoading={isAnyExportLoading} />
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
> >
<div className='flex flex-row items-center gap-1.5'> <Button
<Icon variant='ghost'
icon='heroicons:cloud-arrow-down' color='none'
width={20} onClick={handleExportExcel}
height={20} isLoading={isExcelExportLoading}
/> className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
</Button>
<Button
variant='ghost'
color='none'
onClick={handleExportPDF}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
<span>Export</span> useEffectHook(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
<div className='w-px self-stretch bg-base-content/10' /> return null;
};
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
</Button>
<Button
variant='ghost'
color='none'
onClick={handleExportPDF}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [ }, [
tabId, tabId,
formik.values, filterParams,
isAnyExportLoading, isAnyExportLoading,
filterModal.open, handleExportExcel,
setTabActions, handleExportPDF,
isExcelExportLoading,
isPdfExportLoading,
]); ]);
useEffectHook(() => { const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => { const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => {
const tableColumns: ColumnDef<HppPerKandangReport['rows'][0]>[] = [ const tableColumns: ColumnDef<HppPerKandangReport['rows'][0]>[] = [
@@ -767,6 +785,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
return ( return (
<> <>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'> <div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? ( {!isSubmitted ? (
<HppPerKandangSkeleton <HppPerKandangSkeleton
@@ -1,6 +1,12 @@
'use client'; 'use client';
import React, { useState, useCallback, useEffect, useMemo } from 'react'; import React, {
useState,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { generateProductionResultExcel } from '../export/ProductionResultExportXLSX'; import { generateProductionResultExcel } from '../export/ProductionResultExportXLSX';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -66,6 +72,8 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal(); const filterModal = useModal();
// ===== TABLE COLUMNS ===== // ===== TABLE COLUMNS =====
@@ -242,6 +250,11 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
}, },
}); });
handleFilterModalOpenRef.current = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== OPTIONS ===== // ===== OPTIONS =====
const { const {
setInputValue: setAreaInputValue, setInputValue: setAreaInputValue,
@@ -519,91 +532,108 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
setIsPdfExportLoading(false); setIsPdfExportLoading(false);
}, [filterParams]); }, [filterParams]);
// ===== REGISTER TAB ACTIONS TO STORE ===== // ===== TAB ACTIONS COMPONENT =====
const setTabActions = useTabActionsStore((state) => state.setTabActions); const TabActions = useMemo(() => {
const clearTabActions = useTabActionsStore((state) => state.clearTabActions); return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffect(() => { useEffect(() => {
setTabActions( setTabActions(
tabId, tabId,
<div className='flex flex-row gap-3'> <div className='flex flex-row gap-3'>
<ButtonFilter <ButtonFilter
values={filterParams} values={filterParams}
onClick={() => filterModal.openModal()} onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline' variant='outline'
color='none' className='px-3 py-2.5'
isLoading={isAnyExportLoading} />
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
> >
<div className='flex flex-row items-center gap-1.5'> <Button
<Icon variant='ghost'
icon='heroicons:cloud-arrow-down' color='none'
width={20} onClick={exportToExcelHandler}
height={20} isLoading={isExcelExportLoading}
/> className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
</Button>
<Button
variant='ghost'
color='none'
onClick={exportToPdfHandler}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
<span>Export</span> useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
<div className='w-px self-stretch bg-base-content/10' /> return null;
};
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={exportToExcelHandler}
isLoading={isExcelExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
</Button>
<Button
variant='ghost'
color='none'
onClick={exportToPdfHandler}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [ }, [
tabId, tabId,
filterParams, filterParams,
isAnyExportLoading, isAnyExportLoading,
exportToExcelHandler, exportToExcelHandler,
exportToPdfHandler, exportToPdfHandler,
setTabActions, isExcelExportLoading,
isPdfExportLoading,
]); ]);
useEffect(() => { // Render the TabActions component
return () => { const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
return ( return (
<> <>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'> <div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? ( {!isSubmitted ? (
<ProductionResultSkeleton <ProductionResultSkeleton