16 KiB
LTI Web Client
Next.js 15 (App Router) + React 19 + TypeScript front-end for the LTI ERP system.
Tech stack
- Framework: Next.js 15.5 (App Router, Turbopack)
- UI: React 19, Tailwind CSS v4, Radix UI, daisyUI, lucide-react
- State: zustand
- Forms: Formik + Yup, react-hook-form
- Data fetching: axios + SWR (custom
httpClient/httpClientFetcherinsrc/services/http) - Tables: @tanstack/react-table
- Reporting: @react-pdf/renderer, jspdf, exceljs, xlsx, recharts
Scripts
npm run dev— lint + dev server (Turbopack)npm run build— production buildnpm run lint— ESLintnpm run typecheck—next typegen && tsc --noEmitnpm run format— Prettiernpm run pre-commit— format + lint + typecheck + build (Husky pre-commit hook)
Project structure
src/
app/ # Next.js App Router routes (one folder per feature)
components/
pages/{feature}/ # Page-specific components (mirrors src/app)
helper/ # Cross-cutting helpers (e.g. SuspenseHelper)
ui/ # Shared UI primitives
services/
api/ # API service classes (extend BaseApiService)
http/ # httpClient / httpClientFetcher
hooks/ # Service-level hooks
stores/ # zustand stores grouped by domain
types/api/ # Request/response types per feature
lib/ # Shared helpers (api-helper, formik-helper, utils, validation, …)
config/, styles/
Feature development standard
Always follow this order when adding a new feature. This is a team convention — deviating creates churn in code review.
- Types — Define payload and response types in
src/types/api/{feature}(or{feature}.d.tsfor small features). - API service — Add
src/services/api/{feature}.tsexporting a class that extendsBaseApiService<T, CreatePayload, UpdatePayload>from src/services/api/base.ts. Use a subfolder (e.g.src/services/api/daily-checklist/) when the feature has multiple resource classes. - Page — Create the route under
src/app/{feature}and a matchingsrc/components/pages/{feature}folder for its components. - Component slicing — Break the page UI into components inside
src/components/pages/{feature}. - Wire up the API — Consume the service class from step 2 inside the page/components (often via SWR).
- Detail layout — When a route reads URL params via
useSearchParams(e.g./feature/detail?id=123), addsrc/app/{feature}/detail/layout.tsxthat wrapschildrenin<SuspenseHelper>from@/components/helper/SuspenseHelper. - Shared state — Use zustand stores in
src/stores/{domain}when state must cross component boundaries. - Helpers — Reuse from src/lib first (
api-helper.ts,formik-helper.ts,utils/,validation/, etc.). Add new helpers there.
Reference implementations
closing, finance, expense, production, inventory, marketing, master-data, purchase, report, daily-checklist, dashboard — all live in both src/app/{feature} and src/components/pages/{feature} and follow the standard above.
Conventions
- Path alias
@/maps tosrc/. - Detail pages that read
useSearchParamsMUST be wrapped in<SuspenseHelper>via alayout.tsx(see src/app/finance/detail/layout.tsx for the canonical pattern). - API service classes inherit CRUD methods (
getAll,getSingle, etc.) fromBaseApiService— extend the class for feature-specific endpoints rather than callinghttpClientdirectly 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):
-
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
-
Pass
trueas last parameter to updateFilter callsupdateFilter('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
-
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
onClickand formonResethandler
- Clear each filter with
Optimization: Avoid useCallback for simple handlers
-
useCallbackadds overhead and is only useful for complex logic or memoized child components -
Simple pass-through handlers don't need it:
// ✅ 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).
// Type the useTableFilter with the filter state structure
const { state: tableFilterState, updateFilter, ... } = useTableFilter<{
search: string;
locationFilter?: OptionType<string>;
picFilter?: OptionType<string>;
}>({
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<KandangFilterType>({
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)
<SelectInput
value={formik.values.location}
onChange={handleFilterLocationChange}
...
/>
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,CustomersTableinsrc/components/pages/master-data/- Use same pattern for data tables in other modules (inventory, finance, purchase, etc.)
Server-side sorting pattern
Data tables use TanStack Table's SortingState wired to useTableFilter so that sorting triggers a server re-fetch rather than client-side reordering.
Four-part wiring:
-
Local sort state —
const [sorting, setSorting] = useState<SortingState>([]); -
useTableFilterconfig — Addsort_byandorder_bytoinitialandparamMap. TheparamMapkey is the internal name; the value is the query param name sent to the server (they can differ, e.g.order_by→sort_order):initial: { sort_by: '', order_by: '' } paramMap: { sort_by: 'sort_by', order_by: 'sort_order' } -
useEffectsync — Watchessortingand pushes changes intouseTableFilter:useEffect(() => { if (sorting.length > 0) { updateFilter('sort_by', sorting[0].id, true); updateFilter('order_by', sorting[0].desc ? 'desc' : 'asc', true); } else { updateFilter('sort_by', ''); updateFilter('order_by', ''); } }, [sorting]); -
SWR key — SWR uses
getTableFilterToQueryString()as its key, so any filter change (including sort) automatically re-fetches with the new query params. TanStack Table's built-in client sorting is effectively disabled; the server does the sorting.
Pass sorting, setSorting, and manualSorting to <Table>:
<Table sorting={sorting} setSorting={handleSortingChange} manualSorting={true} ... />
manualSorting={true} is required — without it TanStack Table still applies its own client-side sort pass on top of the server-sorted data, producing incorrect order.
Reference implementation: MarketingTable in src/components/pages/marketing/MarketingTable.tsx.
Server-side file export pattern
All file exports (Excel, PDF, or any format) must use server-side generation — the server returns a binary blob and the browser triggers a download. Never generate files client-side with xlsx, @react-pdf/renderer, jspdf, or similar libraries.
Rule: Export methods live in the API service class, not in components. Components only build the query string and call the service method.
Service method (in src/services/api/{feature}.ts)
async exportToExcel(initialQueryString: string) {
const params = new URLSearchParams(initialQueryString);
params.set('export', 'excel'); // or 'pdf', 'csv', etc.
params.set('page', '1');
params.set('limit', '99999999999');
const res = await httpClient<Blob>(`${this.basePath}?${params.toString()}`, {
method: 'GET',
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `filename-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`);
document.body.appendChild(link);
link.click();
link.remove();
}
- Change
export=excel→export=pdf(and the file extension) for PDF exports. - Add one method per format; keep them side-by-side in the same service class.
Component handler (in the page/tab component)
const handleExportExcel = useCallback(async () => {
setIsExcelExportLoading(true);
try {
const params = new URLSearchParams();
if (filterParams.foo) params.set('foo', filterParams.foo);
// ... map all active filter params ...
await FeatureApi.exportToExcel(params.toString());
toast.success('Excel berhasil dibuat dan diunduh.');
} catch {
toast.error('Gagal membuat Excel. Silakan coba lagi.');
} finally {
setIsExcelExportLoading(false);
}
}, [filterParams, searchValue]);
- Do not fetch all rows into the component to build the file — delegate entirely to the service method.
- Do not import
xlsx,@react-pdf/renderer,jspdf,exceljsin page/tab components.
Reference implementation: MarketingReportApiService.exportDailyMarketingToExcel / exportDailyMarketingToPDF in src/services/api/report/marketing-report.ts, consumed by src/components/pages/report/marketing/tab/DailyMarketingTab.tsx.
RTK (Rust Token Killer) - Token-Optimized Commands
Golden Rule
Always prefix commands with rtk. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use.
Important: Even in command chains with &&, use rtk:
# ❌ Wrong
git add . && git commit -m "msg" && git push
# ✅ Correct
rtk git add . && rtk git commit -m "msg" && rtk git push
RTK Commands by Workflow
Build & Compile (80-90% savings)
rtk cargo build # Cargo build output
rtk cargo check # Cargo check output
rtk cargo clippy # Clippy warnings grouped by file (80%)
rtk tsc # TypeScript errors grouped by file/code (83%)
rtk lint # ESLint/Biome violations grouped (84%)
rtk prettier --check # Files needing format only (70%)
rtk next build # Next.js build with route metrics (87%)
Test (60-99% savings)
rtk cargo test # Cargo test failures only (90%)
rtk go test # Go test failures only (90%)
rtk jest # Jest failures only (99.5%)
rtk vitest # Vitest failures only (99.5%)
rtk playwright test # Playwright failures only (94%)
rtk pytest # Python test failures only (90%)
rtk rake test # Ruby test failures only (90%)
rtk rspec # RSpec test failures only (60%)
rtk test <cmd> # Generic test wrapper - failures only
Git (59-80% savings)
rtk git status # Compact status
rtk git log # Compact log (works with all git flags)
rtk git diff # Compact diff (80%)
rtk git show # Compact show (80%)
rtk git add # Ultra-compact confirmations (59%)
rtk git commit # Ultra-compact confirmations (59%)
rtk git push # Ultra-compact confirmations
rtk git pull # Ultra-compact confirmations
rtk git branch # Compact branch list
rtk git fetch # Compact fetch
rtk git stash # Compact stash
rtk git worktree # Compact worktree
Note: Git passthrough works for ALL subcommands, even those not explicitly listed.
GitHub (26-87% savings)
rtk gh pr view <num> # Compact PR view (87%)
rtk gh pr checks # Compact PR checks (79%)
rtk gh run list # Compact workflow runs (82%)
rtk gh issue list # Compact issue list (80%)
rtk gh api # Compact API responses (26%)
JavaScript/TypeScript Tooling (70-90% savings)
rtk pnpm list # Compact dependency tree (70%)
rtk pnpm outdated # Compact outdated packages (80%)
rtk pnpm install # Compact install output (90%)
rtk npm run <script> # Compact npm script output
rtk npx <cmd> # Compact npx command output
rtk prisma # Prisma without ASCII art (88%)
Files & Search (60-75% savings)
rtk ls <path> # Tree format, compact (65%)
rtk read <file> # Code reading with filtering (60%)
rtk grep <pattern> # Search grouped by file (75%)
rtk find <pattern> # Find grouped by directory (70%)
Analysis & Debug (70-90% savings)
rtk err <cmd> # Filter errors only from any command
rtk log <file> # Deduplicated logs with counts
rtk json <file> # JSON structure without values
rtk deps # Dependency overview
rtk env # Environment variables compact
rtk summary <cmd> # Smart summary of command output
rtk diff # Ultra-compact diffs
Infrastructure (85% savings)
rtk docker ps # Compact container list
rtk docker images # Compact image list
rtk docker logs <c> # Deduplicated logs
rtk kubectl get # Compact resource list
rtk kubectl logs # Deduplicated pod logs
Network (65-70% savings)
rtk curl <url> # Compact HTTP responses (70%)
rtk wget <url> # Compact download output (65%)
Meta Commands
rtk gain # View token savings statistics
rtk gain --history # View command history with savings
rtk discover # Analyze Claude Code sessions for missed RTK usage
rtk proxy <cmd> # Run command without filtering (for debugging)
rtk init # Add RTK instructions to CLAUDE.md
rtk init --global # Add RTK to ~/.claude/CLAUDE.md
Token Savings Overview
| Category | Commands | Typical Savings |
|---|---|---|
| Tests | vitest, playwright, cargo test | 90-99% |
| Build | next, tsc, lint, prettier | 70-87% |
| Git | status, log, diff, add, commit | 59-80% |
| GitHub | gh pr, gh run, gh issue | 26-87% |
| Package Managers | pnpm, npm, npx | 70-90% |
| Files | ls, read, grep, find | 60-75% |
| Infrastructure | docker, kubectl | 85% |
| Network | curl, wget | 65-70% |
Overall average: 60-90% token reduction on common development operations.