Merge branch 'fix/project-flock' into 'development'

[FIX/FE] Project Flock

See merge request mbugroup/lti-web-client!412
This commit is contained in:
Rivaldi A N S
2026-04-21 09:01:14 +00:00
7 changed files with 135 additions and 24 deletions
+1 -1
View File
@@ -523,7 +523,7 @@ const useSelect = <T,>(
const qs = new URLSearchParams({ const qs = new URLSearchParams({
...(params ?? {}), ...(params ?? {}),
[searchKey]: inputValue ?? '', [searchKey ? searchKey : 'search']: inputValue ?? '',
[pageKey]: String(pageIndex + 1), [pageKey]: String(pageIndex + 1),
[limitKey]: String(limit), [limitKey]: String(limit),
}).toString(); }).toString();
@@ -23,7 +23,6 @@ import { Icon } from '@iconify/react';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname } from 'next/navigation';
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import { useUiStore } from '@/stores/ui/ui.store';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import useSWR from 'swr'; import useSWR from 'swr';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
@@ -148,7 +147,6 @@ const RowOptionsMenu = ({
}; };
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const { searchValue, setSearchValue, setTableState } = useUiStore();
const pathname = usePathname(); const pathname = usePathname();
const isSuccess = useProjectFlockStore((s) => s.isSuccess); const isSuccess = useProjectFlockStore((s) => s.isSuccess);
@@ -185,7 +183,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
category: 'category', category: 'category',
period: 'period', period: 'period',
}, },
persist: true,
storeName: 'project-flock-table',
}); });
const router = useRouter(); const router = useRouter();
// ===== State ===== // ===== State =====
@@ -425,18 +427,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
setRowSelection({}); setRowSelection({});
}; };
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
useEffect(() => {
setTableState('project-flock-table', pathname);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => { const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
updateFilter('search', e.target.value); updateFilter('search', e.target.value);
}; };
const confirmApprovalHandler = async ( const confirmApprovalHandler = async (
notes: string, notes: string,
approvalAction: 'APPROVED' | 'REJECTED' approvalAction: 'APPROVED' | 'REJECTED'
@@ -261,7 +261,7 @@ const ProjectFlockForm = ({
isLoadingOptions: isLoadingFlocks, isLoadingOptions: isLoadingFlocks,
options: optionsFlock, options: optionsFlock,
loadMore: loadMoreFlock, loadMore: loadMoreFlock,
} = useSelect(FlockApi.basePath, 'id', 'name', '', { } = useSelect(FlockApi.basePath, 'id', 'name', 'search', {
project_category: selectedCategory, project_category: selectedCategory,
location_id: selectedLocation, location_id: selectedLocation,
area_id: selectedArea, area_id: selectedArea,
@@ -279,7 +279,7 @@ const ProjectFlockForm = ({
isLoadingOptions: isLoadingLocations, isLoadingOptions: isLoadingLocations,
setInputValue: setInputValueLocation, setInputValue: setInputValueLocation,
loadMore: loadMoreLocation, loadMore: loadMoreLocation,
} = useSelect(LocationApi.basePath, 'id', 'name', '', { } = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
area_id: area_id:
selectedArea != '' selectedArea != ''
? selectedArea ? selectedArea
@@ -291,7 +291,7 @@ const ProjectFlockForm = ({
isLoadingOptions: isLoadingProductionStandards, isLoadingOptions: isLoadingProductionStandards,
setInputValue: setInputValueProductionStandard, setInputValue: setInputValueProductionStandard,
loadMore: loadMoreProductionStandard, loadMore: loadMoreProductionStandard,
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', { } = useSelect(ProductionStandardApi.basePath, 'id', 'name', 'search', {
project_category: selectedCategory, project_category: selectedCategory,
}); });
@@ -307,7 +307,7 @@ const ProjectFlockForm = ({
} = useSWR(kandangUrl, KandangApi.getAllFetcher); } = useSWR(kandangUrl, KandangApi.getAllFetcher);
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR( const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
`${selectedFlock?.toString()}/periods`, selectedFlock ? `${selectedFlock?.toString()}/periods` : undefined,
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string)) () => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
); );
@@ -793,6 +793,7 @@ const ProjectFlockForm = ({
formik.values.kandang_ids?.includes(kandang.id) formik.values.kandang_ids?.includes(kandang.id)
)?.period )?.period
: undefined; : undefined;
const inputPeriod = const inputPeriod =
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod; (initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
+54 -8
View File
@@ -1,4 +1,5 @@
import { useCallback, useMemo, useReducer } from 'react'; import { useCallback, useEffect, useMemo, useReducer } from 'react';
import { useTableFilterStore } from '@/stores/table/table-filter.store';
/** Core filter shape (page + pageSize) extended by your custom fields */ /** Core filter shape (page + pageSize) extended by your custom fields */
export type TableFilterState<TExtra extends Record<string, unknown>> = { export type TableFilterState<TExtra extends Record<string, unknown>> = {
@@ -30,6 +31,9 @@ export type UseTableFilterOptions<TExtra extends Record<string, unknown>> = {
paramMap?: Partial<Record<keyof TableFilterState<TExtra>, string>>; paramMap?: Partial<Record<keyof TableFilterState<TExtra>, string>>;
/** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */ /** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */
omitDefaultsInUrl?: boolean; omitDefaultsInUrl?: boolean;
persist?: boolean;
storeName?: string;
}; };
function clampToInt(n: number, min = 1) { function clampToInt(n: number, min = 1) {
@@ -90,9 +94,37 @@ function shallowEqual<T extends Record<string, unknown>>(
export function useTableFilter<TExtra extends Record<string, unknown>>( export function useTableFilter<TExtra extends Record<string, unknown>>(
options?: UseTableFilterOptions<TExtra> options?: UseTableFilterOptions<TExtra>
) { ) {
const defaults = useMemo( if (options?.persist && !options?.storeName) {
() => createInitialState<TExtra>(options), throw new Error(
[options] 'storeName is required if persist is true in useTableFilter!'
);
}
const storeName = options?.storeName ?? '';
const persistedState = useTableFilterStore(
useCallback(
(storeState) =>
storeName
? (storeState.data[storeName] as Partial<TableFilterState<TExtra>>)
: undefined,
[storeName]
)
);
const setTableData = useTableFilterStore(
(storeState) => storeState.setTableData
);
const defaults = useMemo(() => {
return createInitialState<TExtra>(options);
}, [options]);
const initialState = useMemo(
() =>
({
...defaults,
...(persistedState as object),
}) as TableFilterState<TExtra>,
[defaults, persistedState]
); );
const [state, dispatch] = useReducer( const [state, dispatch] = useReducer(
@@ -106,15 +138,22 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
case 'SET_PAGE_SIZE': { case 'SET_PAGE_SIZE': {
const pageSize = clampToInt(a.pageSize); const pageSize = clampToInt(a.pageSize);
const page = a.resetPage ? 1 : s.page; const page = a.resetPage ? 1 : s.page;
return { ...s, pageSize, page }; return { ...s, pageSize, page };
} }
case 'SET_FILTERS': { case 'SET_FILTERS': {
const page = a.resetPage ? 1 : s.page; const page = a.resetPage ? 1 : s.page;
return { ...s, ...a.filters, page }; return { ...s, ...a.filters, page };
} }
case 'UPDATE_FILTER': { case 'UPDATE_FILTER': {
const page = a.resetPage ? 1 : s.page; const page = a.resetPage ? 1 : s.page;
return { ...s, [a.key]: a.value, page } as TableFilterState<TExtra>;
return {
...s,
[a.key]: a.value,
page,
} as TableFilterState<TExtra>;
} }
case 'REPLACE_ALL': case 'REPLACE_ALL':
return { return {
@@ -128,12 +167,19 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
return s; return s;
} }
}, },
defaults initialState
); );
// Notify consumer on change (stable ref) useEffect(() => {
if (!options?.persist || !storeName) {
return;
}
setTableData(storeName, state);
}, [options?.persist, setTableData, state, storeName]);
const onChange = options?.onChange; const onChange = options?.onChange;
useMemo(() => { useEffect(() => {
if (onChange) onChange(state); if (onChange) onChange(state);
}, [state, onChange]); }, [state, onChange]);
+60
View File
@@ -0,0 +1,60 @@
import { create } from 'zustand';
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
import { TableFilterStore } from '@/types/stores';
type TableFilterStoreState = TableFilterStore<
Record<string, Record<string, unknown>>
>;
export const useTableFilterStore = create<TableFilterStoreState>()(
devtools(
persist(
(set) => ({
data: {},
setData: (newData) => {
set({ data: newData });
},
setTableData: (key, tableData) => {
set((state) => ({
data: {
...state.data,
[key]: tableData,
},
}));
},
setTableDataField: (key, field, value) => {
set((state) => ({
data: {
...state.data,
[key]: {
...state.data[key],
[field]: value,
},
},
}));
},
setSearchValue: (key, searchValue) => {
set((state) => ({
data: {
...state.data,
[key]: {
...state.data[key],
// search key
search: searchValue,
},
},
}));
},
}),
{
name: 'table-filter-store',
storage: createJSONStorage(() => sessionStorage),
}
)
)
);
+2 -1
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { createJSONStorage, devtools, persist } from 'zustand/middleware';
import { UIStore } from '@/types/stores'; import { UIStore } from '@/types/stores';
import { createMainUiSlice } from '@/stores/ui/slices/main.slice'; import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
@@ -20,6 +20,7 @@ export const useUiStore = create<UIStore>()(
}), }),
{ {
name: 'search-store', name: 'search-store',
storage: createJSONStorage(() => sessionStorage),
partialize: (state) => ({ partialize: (state) => ({
key: state.key, key: state.key,
path: state.path, path: state.path,
+8
View File
@@ -117,3 +117,11 @@ export type ProjectFlockSlice = {
setCreatedProjectFlock: (data: ProjectFlock | null) => void; setCreatedProjectFlock: (data: ProjectFlock | null) => void;
resetProjectFlock: () => void; resetProjectFlock: () => void;
}; };
export type TableFilterStore<T = Record<string, Record<string, unknown>>> = {
data: T;
setData: (newData: T) => void;
setTableData: (key: string, tableData: Record<string, unknown>) => void;
setTableDataField: (key: string, field: string, value: unknown) => void;
setSearchValue: (key: string, searchValue: string) => void;
};