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({
...(params ?? {}),
[searchKey]: inputValue ?? '',
[searchKey ? searchKey : 'search']: inputValue ?? '',
[pageKey]: String(pageIndex + 1),
[limitKey]: String(limit),
}).toString();
@@ -23,7 +23,6 @@ import { Icon } from '@iconify/react';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { useRouter, usePathname } from 'next/navigation';
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import { useUiStore } from '@/stores/ui/ui.store';
import toast from 'react-hot-toast';
import useSWR from 'swr';
import { useFormik } from 'formik';
@@ -148,7 +147,6 @@ const RowOptionsMenu = ({
};
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const { searchValue, setSearchValue, setTableState } = useUiStore();
const pathname = usePathname();
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
@@ -185,7 +183,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
category: 'category',
period: 'period',
},
persist: true,
storeName: 'project-flock-table',
});
const router = useRouter();
// ===== State =====
@@ -425,18 +427,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
setIsDeleteLoading(false);
setRowSelection({});
};
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
useEffect(() => {
setTableState('project-flock-table', pathname);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
updateFilter('search', e.target.value);
};
const confirmApprovalHandler = async (
notes: string,
approvalAction: 'APPROVED' | 'REJECTED'
@@ -261,7 +261,7 @@ const ProjectFlockForm = ({
isLoadingOptions: isLoadingFlocks,
options: optionsFlock,
loadMore: loadMoreFlock,
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
} = useSelect(FlockApi.basePath, 'id', 'name', 'search', {
project_category: selectedCategory,
location_id: selectedLocation,
area_id: selectedArea,
@@ -279,7 +279,7 @@ const ProjectFlockForm = ({
isLoadingOptions: isLoadingLocations,
setInputValue: setInputValueLocation,
loadMore: loadMoreLocation,
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
area_id:
selectedArea != ''
? selectedArea
@@ -291,7 +291,7 @@ const ProjectFlockForm = ({
isLoadingOptions: isLoadingProductionStandards,
setInputValue: setInputValueProductionStandard,
loadMore: loadMoreProductionStandard,
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', 'search', {
project_category: selectedCategory,
});
@@ -307,7 +307,7 @@ const ProjectFlockForm = ({
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
`${selectedFlock?.toString()}/periods`,
selectedFlock ? `${selectedFlock?.toString()}/periods` : undefined,
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
);
@@ -793,6 +793,7 @@ const ProjectFlockForm = ({
formik.values.kandang_ids?.includes(kandang.id)
)?.period
: undefined;
const inputPeriod =
(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 */
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>>;
/** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */
omitDefaultsInUrl?: boolean;
persist?: boolean;
storeName?: string;
};
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>>(
options?: UseTableFilterOptions<TExtra>
) {
const defaults = useMemo(
() => createInitialState<TExtra>(options),
[options]
if (options?.persist && !options?.storeName) {
throw new Error(
'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(
@@ -106,15 +138,22 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
case 'SET_PAGE_SIZE': {
const pageSize = clampToInt(a.pageSize);
const page = a.resetPage ? 1 : s.page;
return { ...s, pageSize, page };
}
case 'SET_FILTERS': {
const page = a.resetPage ? 1 : s.page;
return { ...s, ...a.filters, page };
}
case 'UPDATE_FILTER': {
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':
return {
@@ -128,12 +167,19 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
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;
useMemo(() => {
useEffect(() => {
if (onChange) onChange(state);
}, [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';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
import { UIStore } from '@/types/stores';
import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
@@ -20,6 +20,7 @@ export const useUiStore = create<UIStore>()(
}),
{
name: 'search-store',
storage: createJSONStorage(() => sessionStorage),
partialize: (state) => ({
key: state.key,
path: state.path,
+8
View File
@@ -117,3 +117,11 @@ export type ProjectFlockSlice = {
setCreatedProjectFlock: (data: ProjectFlock | null) => 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;
};