diff --git a/CLAUDE.md b/CLAUDE.md index 8b6b07a2..b1025264 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,3 +63,100 @@ src/ - Detail pages that read `useSearchParams` MUST be wrapped in `` via a `layout.tsx` (see [src/app/finance/detail/layout.tsx](src/app/finance/detail/layout.tsx) for the canonical pattern). - API service classes inherit CRUD methods (`getAll`, `getSingle`, etc.) from `BaseApiService` — extend the class for feature-specific endpoints rather than calling `httpClient` directly from components. - Pre-commit runs format + lint + typecheck + build; do not bypass with `--no-verify`. + +## Table filter persistence pattern + +Data tables across all modules (master-data, inventory, finance, purchase, etc.) use `useTableFilter` with `persist: true` to persist filter state in localStorage. This allows users' search, pagination, and filter choices to survive page refreshes. + +**Three core principles (apply to all table components):** + +1. **Set formik initialValues from tableFilterState** (not hardcoded defaults) + - Ensures the filter modal displays currently active filters when opened + - Initialize directly from persisted state: `location: tableFilterState.locationFilter` + +2. **Pass `true` as last parameter to updateFilter calls** + - `updateFilter('fieldName', value, true)` immediately persists to localStorage + - Resets pagination to page 1 when filters change (via SWR revalidation) + - Apply to: search handlers, filter form submissions, reset handlers + +3. **Create custom formikResetHandler function** + - Clear each filter with `updateFilter(fieldName, defaultValue, true)` + - Call `formik.resetForm({ values: { ...defaults } })` + - Close the modal at the end + - Attach to both button `onClick` and form `onReset` handler + +**Optimization: Avoid useCallback for simple handlers** + +- `useCallback` adds overhead and is only useful for complex logic or memoized child components +- Simple pass-through handlers don't need it: + + ```tsx + // ✅ Good: Simple handler without useCallback + const handleFilterChange = (val) => setFieldValue('location', val); + + // ❌ Avoid: Unnecessary useCallback overhead + const handleFilterChange = useCallback( + (val) => setFieldValue('location', val), + [setFieldValue] + ); + ``` + +**Best practice: Store OptionType objects directly, not IDs** + +For select inputs, store the complete `OptionType` object in both formik state and tableFilterState. This eliminates the need for computed helper values (like searching options arrays to find the matching object). + +```tsx +// Type the useTableFilter with the filter state structure +const { state: tableFilterState, updateFilter, ... } = useTableFilter<{ + search: string; + locationFilter?: OptionType; + picFilter?: OptionType; +}>({ + initial: { + search: '', + locationFilter: undefined, + picFilter: undefined + }, + paramMap: { + page: 'page', + pageSize: 'limit', + locationFilter: 'location_id', + picFilter: 'pic_id', + }, + persist: true, + storeName: 'kandangs-table', +}); + +// Initialize formik with tableFilterState values (now typed OptionType objects) +const formik = useFormik({ + initialValues: { + location: tableFilterState.locationFilter, + pic: tableFilterState.picFilter, + }, + ... +}); + +// Handlers store the complete OptionType, not just the ID +const handleFilterLocationChange = useCallback( + (val) => setFieldValue('location', val), + [setFieldValue] +); + +// Use formik values directly in select inputs (no computed helpers needed) + +``` + +**Apply this pattern to:** + +- Any data table component across any module that needs persistent filters +- Master-data tables, inventory lists, finance reports, purchase orders, etc. +- Whenever users' filter/search/pagination choices should survive page refreshes + +**Reference implementations:** + +- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/` +- Use same pattern for data tables in other modules (inventory, finance, purchase, etc.)