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

[FIX/FE] Project Flock Form

See merge request mbugroup/lti-web-client!436
This commit is contained in:
Rivaldi A N S
2026-04-27 04:02:09 +00:00
16 changed files with 409 additions and 145 deletions
+65
View File
@@ -0,0 +1,65 @@
# 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` / `httpClientFetcher` in `src/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 build
- `npm run lint` — ESLint
- `npm run typecheck``next typegen && tsc --noEmit`
- `npm run format` — Prettier
- `npm 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.
1. **Types** — Define payload and response types in `src/types/api/{feature}` (or `{feature}.d.ts` for small features).
2. **API service** — Add `src/services/api/{feature}.ts` exporting a class that extends `BaseApiService<T, CreatePayload, UpdatePayload>` from [src/services/api/base.ts](src/services/api/base.ts). Use a subfolder (e.g. `src/services/api/daily-checklist/`) when the feature has multiple resource classes.
3. **Page** — Create the route under `src/app/{feature}` and a matching `src/components/pages/{feature}` folder for its components.
4. **Component slicing** — Break the page UI into components inside `src/components/pages/{feature}`.
5. **Wire up the API** — Consume the service class from step 2 inside the page/components (often via SWR).
6. **Detail layout** — When a route reads URL params via `useSearchParams` (e.g. `/feature/detail?id=123`), add `src/app/{feature}/detail/layout.tsx` that wraps `children` in `<SuspenseHelper>` from `@/components/helper/SuspenseHelper`.
7. **Shared state** — Use zustand stores in `src/stores/{domain}` when state must cross component boundaries.
8. **Helpers** — Reuse from [src/lib](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 to `src/`.
- Detail pages that read `useSearchParams` MUST be wrapped in `<SuspenseHelper>` 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`.
@@ -265,7 +265,11 @@ const FinanceTable = () => {
updateFilter('endDate', values.end_date);
// Save display names for restoration on modal reopen
const toNames = (val: OptionType | OptionType[] | null) =>
val ? (Array.isArray(val) ? val : [val]).map((o) => String(o.label)).join(',') : '';
val
? (Array.isArray(val) ? val : [val])
.map((o) => String(o.label))
.join(',')
: '';
updateFilter('bankNames', toNames(selectedBank));
updateFilter('customerNames', toNames(selectedCustomerId));
updateFilter('supplierNames', toNames(selectedSupplierId));
@@ -516,8 +520,9 @@ const FinanceTable = () => {
// Restore sort by
const restoredSortBy =
sortByOptions.find((opt) => String(opt.value) === tableFilterState.sortBy) ||
null;
sortByOptions.find(
(opt) => String(opt.value) === tableFilterState.sortBy
) || null;
setSelectedSortBy(restoredSortBy);
// Restore formik values
@@ -153,8 +153,14 @@ const InventoryAdjustmentTable = () => {
updateFilter('productFilter', values.product_id || '');
updateFilter('warehouseFilter', values.warehouse_id || '');
updateFilter('transactionTypeFilter', values.transaction_type || '');
updateFilter('productName', productIdValue?.label ? String(productIdValue.label) : '');
updateFilter('warehouseName', warehouseIdValue?.label ? String(warehouseIdValue.label) : '');
updateFilter(
'productName',
productIdValue?.label ? String(productIdValue.label) : ''
);
updateFilter(
'warehouseName',
warehouseIdValue?.label ? String(warehouseIdValue.label) : ''
);
filterModal.closeModal();
setSubmitting(false);
},
@@ -216,7 +222,10 @@ const InventoryAdjustmentTable = () => {
val: OptionType | OptionType[] | null
) => {
const warehouse = val as OptionType | null;
formik.setFieldValue('warehouse_id', warehouse?.value ? String(warehouse.value) : null);
formik.setFieldValue(
'warehouse_id',
warehouse?.value ? String(warehouse.value) : null
);
};
const handleFilterTransactionTypeChange = useCallback(
@@ -236,7 +245,10 @@ const InventoryAdjustmentTable = () => {
);
if (found) return found;
if (tableFilterState.productName) {
return { value: formik.values.product_id, label: tableFilterState.productName };
return {
value: formik.values.product_id,
label: tableFilterState.productName,
};
}
return null;
}, [formik.values.product_id, productOptions, tableFilterState.productName]);
@@ -248,10 +260,17 @@ const InventoryAdjustmentTable = () => {
);
if (found) return found;
if (tableFilterState.warehouseName) {
return { value: formik.values.warehouse_id, label: tableFilterState.warehouseName };
return {
value: formik.values.warehouse_id,
label: tableFilterState.warehouseName,
};
}
return null;
}, [formik.values.warehouse_id, warehouseOptions, tableFilterState.warehouseName]);
}, [
formik.values.warehouse_id,
warehouseOptions,
tableFilterState.warehouseName,
]);
const transactionTypeValue = useMemo(() => {
if (!formik.values.transaction_type) return null;
@@ -149,8 +149,14 @@ const MovementTable = () => {
onSubmit: (values, { setSubmitting }) => {
updateFilter('productFilter', values.product_id || '');
updateFilter('warehouseFilter', values.warehouse_id || '');
updateFilter('productName', productIdValue?.label ? String(productIdValue.label) : '');
updateFilter('warehouseName', warehouseIdValue?.label ? String(warehouseIdValue.label) : '');
updateFilter(
'productName',
productIdValue?.label ? String(productIdValue.label) : ''
);
updateFilter(
'warehouseName',
warehouseIdValue?.label ? String(warehouseIdValue.label) : ''
);
filterModal.closeModal();
setSubmitting(false);
},
@@ -216,7 +222,10 @@ const MovementTable = () => {
);
if (found) return found;
if (tableFilterState.productName) {
return { value: formik.values.product_id, label: tableFilterState.productName };
return {
value: formik.values.product_id,
label: tableFilterState.productName,
};
}
return null;
}, [formik.values.product_id, productOptions, tableFilterState.productName]);
@@ -228,10 +237,17 @@ const MovementTable = () => {
);
if (found) return found;
if (tableFilterState.warehouseName) {
return { value: formik.values.warehouse_id, label: tableFilterState.warehouseName };
return {
value: formik.values.warehouse_id,
label: tableFilterState.warehouseName,
};
}
return null;
}, [formik.values.warehouse_id, warehouseOptions, tableFilterState.warehouseName]);
}, [
formik.values.warehouse_id,
warehouseOptions,
tableFilterState.warehouseName,
]);
// ===== HANDLE FILTER MODAL OPEN =====
const handleFilterModalOpen = () => {
@@ -403,7 +419,13 @@ const MovementTable = () => {
<ButtonFilter
values={tableFilterState}
excludeFields={['page', 'pageSize', 'search', 'productName', 'warehouseName']}
excludeFields={[
'page',
'pageSize',
'search',
'productName',
'warehouseName',
]}
onClick={handleFilterModalOpen}
className='px-3 py-2.5'
/>
@@ -113,7 +113,10 @@ const InventoryProductTable = () => {
validationSchema: object().shape({ category_id: string().nullable() }),
onSubmit: (values, { setSubmitting }) => {
updateFilter('categoryFilter', values.category_id || '');
updateFilter('categoryName', categoryIdValue?.label ? String(categoryIdValue.label) : '');
updateFilter(
'categoryName',
categoryIdValue?.label ? String(categoryIdValue.label) : ''
);
filterModal.closeModal();
setSubmitting(false);
},
@@ -145,10 +148,17 @@ const InventoryProductTable = () => {
);
if (found) return found;
if (tableFilterState.categoryName) {
return { value: formik.values.category_id, label: tableFilterState.categoryName };
return {
value: formik.values.category_id,
label: tableFilterState.categoryName,
};
}
return null;
}, [formik.values.category_id, categoryOptions, tableFilterState.categoryName]);
}, [
formik.values.category_id,
categoryOptions,
tableFilterState.categoryName,
]);
// ===== HANDLE FILTER MODAL OPEN =====
const handleFilterModalOpen = () => {
@@ -156,9 +166,14 @@ const InventoryProductTable = () => {
filterModal.openModal();
};
const handleFilterCategoryChange = (val: OptionType | OptionType[] | null) => {
const handleFilterCategoryChange = (
val: OptionType | OptionType[] | null
) => {
const category = val as OptionType | null;
formik.setFieldValue('category_id', category?.value ? String(category.value) : null);
formik.setFieldValue(
'category_id',
category?.value ? String(category.value) : null
);
};
const [sorting, setSorting] = useState<SortingState>([]);
@@ -254,102 +269,106 @@ const InventoryProductTable = () => {
return (
<>
<div className='w-full'>
{/* Header Section */}
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
{/* Action Buttons */}
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.inventory.product_stock.create'>
<Button
href='/inventory/product/add'
color='primary'
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
>
<Icon icon='heroicons:plus' width={20} height={20} />
Add Product
</Button>
</RequirePermission>
</div>
{/* Search and Filter */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput
name='search'
placeholder='Search'
value={tableFilterState.search ?? ''}
onChange={searchChangeHandler}
startAdornment={
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
}
className={{
wrapper: 'w-full min-w-24 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<ButtonFilter
values={tableFilterState}
excludeFields={['page', 'pageSize', 'search', 'categoryName']}
onClick={handleFilterModalOpen}
className='px-3 py-2.5'
/>
</div>
</div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
{isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
<div className='w-full'>
{/* Header Section */}
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
{/* Action Buttons */}
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.inventory.product_stock.create'>
<Button
href='/inventory/product/add'
color='primary'
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
>
<Icon icon='heroicons:plus' width={20} height={20} />
Add Product
</Button>
</RequirePermission>
</div>
) : !isResponseSuccess(inventoryProducts) ||
inventoryProducts.data?.length === 0 ? (
<div className='p-3'>
<InventoryProductTableSkeleton
columns={columns}
icon={
{/* Search and Filter */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput
name='search'
placeholder='Search'
value={tableFilterState.search ?? ''}
onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:document-text'
className='text-white'
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
className={{
wrapper: 'w-full min-w-24 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<ButtonFilter
values={tableFilterState}
excludeFields={['page', 'pageSize', 'search', 'categoryName']}
onClick={handleFilterModalOpen}
className='px-3 py-2.5'
/>
</div>
) : (
<Table<InventoryProduct>
data={
isResponseSuccess(inventoryProducts)
? inventoryProducts?.data
: []
}
columns={columns}
pageSize={tableFilterState.pageSize}
page={
isResponseSuccess(inventoryProducts)
? inventoryProducts?.meta?.page
: 0
}
totalItems={
isResponseSuccess(inventoryProducts)
? inventoryProducts?.meta?.total_results
: 0
}
onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn('p-3 mb-0'),
headerColumnClassName: 'text-nowrap',
}}
/>
)}
</div>
{/* Table Section */}
<div className='flex flex-col mb-4'>
{isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
) : !isResponseSuccess(inventoryProducts) ||
inventoryProducts.data?.length === 0 ? (
<div className='p-3'>
<InventoryProductTableSkeleton
columns={columns}
icon={
<Icon
icon='heroicons:document-text'
className='text-white'
width={20}
height={20}
/>
}
/>
</div>
) : (
<Table<InventoryProduct>
data={
isResponseSuccess(inventoryProducts)
? inventoryProducts?.data
: []
}
columns={columns}
pageSize={tableFilterState.pageSize}
page={
isResponseSuccess(inventoryProducts)
? inventoryProducts?.meta?.page
: 0
}
totalItems={
isResponseSuccess(inventoryProducts)
? inventoryProducts?.meta?.total_results
: 0
}
onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn('p-3 mb-0'),
headerColumnClassName: 'text-nowrap',
}}
/>
)}
</div>
</div>
</div>
{/* Filter Modal */}
<Modal
@@ -262,9 +262,18 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
updateFilter('kandang_id', values.kandang_id || '');
updateFilter('category', values.category || '');
updateFilter('period', values.period || '');
updateFilter('area_name', areaValue?.label ? String(areaValue.label) : '');
updateFilter('location_name', locationValue?.label ? String(locationValue.label) : '');
updateFilter('kandang_name', kandangValue?.label ? String(kandangValue.label) : '');
updateFilter(
'area_name',
areaValue?.label ? String(areaValue.label) : ''
);
updateFilter(
'location_name',
locationValue?.label ? String(locationValue.label) : ''
);
updateFilter(
'kandang_name',
kandangValue?.label ? String(kandangValue.label) : ''
);
filterModal.closeModal();
setSubmitting(false);
},
@@ -329,10 +338,15 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
// ===== FILTER HELPERS =====
const areaValue = useMemo(() => {
if (!formik.values.area_id) return null;
const found = areaOptions.find((opt) => String(opt.value) === formik.values.area_id);
const found = areaOptions.find(
(opt) => String(opt.value) === formik.values.area_id
);
if (found) return found;
if (tableFilterState.area_name) {
return { value: formik.values.area_id, label: tableFilterState.area_name };
return {
value: formik.values.area_id,
label: tableFilterState.area_name,
};
}
return null;
}, [formik.values.area_id, areaOptions, tableFilterState.area_name]);
@@ -344,10 +358,17 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
);
if (found) return found;
if (tableFilterState.location_name) {
return { value: formik.values.location_id, label: tableFilterState.location_name };
return {
value: formik.values.location_id,
label: tableFilterState.location_name,
};
}
return null;
}, [formik.values.location_id, locationOptions, tableFilterState.location_name]);
}, [
formik.values.location_id,
locationOptions,
tableFilterState.location_name,
]);
const kandangValue = useMemo(() => {
if (!formik.values.kandang_id) return null;
@@ -356,7 +377,10 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
);
if (found) return found;
if (tableFilterState.kandang_name) {
return { value: formik.values.kandang_id, label: tableFilterState.kandang_name };
return {
value: formik.values.kandang_id,
label: tableFilterState.kandang_name,
};
}
return null;
}, [formik.values.kandang_id, kandangOptions, tableFilterState.kandang_name]);
@@ -446,7 +470,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
};
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
updateFilter('search', e.target.value, true);
};
const confirmApprovalHandler = async (
@@ -984,7 +1008,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<ButtonFilter
values={tableFilterState}
excludeFields={['page', 'pageSize', 'search', 'area_name', 'location_name', 'kandang_name']}
excludeFields={[
'page',
'pageSize',
'search',
'area_name',
'location_name',
'kandang_name',
]}
onClick={handleFilterModalOpen}
className='px-3 py-2.5'
/>
@@ -26,6 +26,7 @@ type ProjectFlockFormSchemaType = {
label: string;
} | null;
location_id: number;
period: number | string;
kandang_ids: number[];
project_budgets: ProjectFlockBudgetsSchemaType[];
};
@@ -109,6 +110,12 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType
.min(1, 'Lokasi wajib diisi!')
.required('Lokasi wajib diisi!'),
// Period
period: Yup.number()
.typeError('Periode harus berupa angka!')
.min(1, 'Periode minimal 1!')
.required('Periode wajib diisi!'),
kandang_ids: Yup.array()
.of(Yup.number().required('Kandang tidak valid!'))
.min(1, 'Minimal harus ada 1 kandang!')
@@ -152,6 +152,10 @@ export const ProjectFlockFormConfirmationTable = ({
label: 'Standar Produksi',
value: projectFlockForm?.production_standard?.label ?? '-',
},
{
label: 'Periode',
value: projectFlockForm?.period ?? '-',
},
{
label: 'Informasi Kandang',
value: '',
@@ -529,6 +533,7 @@ const ProjectFlockForm = ({
kandang_ids: initialValues?.kandangs?.map(
(k: Kandang) => k.id
) as number[],
period: initialValues?.period ?? '',
project_budgets: initialValues?.project_budgets?.map((budget) => {
return {
nonstock: {
@@ -568,6 +573,7 @@ const ProjectFlockForm = ({
category: values.category as string,
production_standard_id: values.production_standard_id as number,
location_id: values.location_id as number,
period: parseInt(values.period as unknown as string),
kandang_ids: values.kandang_ids as number[],
project_budgets: values.project_budgets.flatMap((budget) => {
return {
@@ -1025,10 +1031,18 @@ const ProjectFlockForm = ({
<NumberInput
name='period'
label='Periode'
disabled
readOnly
placeholder='Periode Flock'
value={selectedLocation ? inputPeriod : ''}
value={formik.values.period}
onChange={(e) =>
formik.setFieldValue('period', e.target.value)
}
onBlur={formik.handleBlur}
allowNegative={false}
decimalScale={0}
isError={
formik.touched.period && Boolean(formik.errors.period)
}
errorMessage={formik.errors.period as string}
/>
</div>
@@ -496,11 +496,23 @@ const TransferToLayingsTable = () => {
updateFilter('startDate', values.startDate || '');
updateFilter('endDate', values.endDate || '');
updateFilter('flockSource', flockSourceOpts.map((o) => String(o.value)).join(','));
updateFilter('flockDestination', flockDestOpts.map((o) => String(o.value)).join(','));
updateFilter(
'flockSource',
flockSourceOpts.map((o) => String(o.value)).join(',')
);
updateFilter(
'flockDestination',
flockDestOpts.map((o) => String(o.value)).join(',')
);
updateFilter('status', statusOpts.map((o) => String(o.value)).join(','));
updateFilter('flockSourceNames', flockSourceOpts.map((o) => String(o.label)).join(','));
updateFilter('flockDestinationNames', flockDestOpts.map((o) => String(o.label)).join(','));
updateFilter(
'flockSourceNames',
flockSourceOpts.map((o) => String(o.label)).join(',')
);
updateFilter(
'flockDestinationNames',
flockDestOpts.map((o) => String(o.label)).join(',')
);
};
const filterResetHandler = () => {
@@ -127,23 +127,37 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
handleFilterModalOpenRef.current = () => {
const restoredLocation = filterParams.location_id
? locationOptions.find((opt) => String(opt.value) === filterParams.location_id) ||
{ value: filterParams.location_id, label: filterParams.location_id }
? locationOptions.find(
(opt) => String(opt.value) === filterParams.location_id
) || {
value: filterParams.location_id,
label: filterParams.location_id,
}
: null;
const restoredSupplier = filterParams.supplier_id
? supplierOptions.find((opt) => String(opt.value) === filterParams.supplier_id) ||
{ value: filterParams.supplier_id, label: filterParams.supplier_id }
? supplierOptions.find(
(opt) => String(opt.value) === filterParams.supplier_id
) || {
value: filterParams.supplier_id,
label: filterParams.supplier_id,
}
: null;
const restoredKandang = filterParams.kandang_id
? projectFlockKandangOptions.find((opt) => String(opt.value) === filterParams.kandang_id) ||
{ value: filterParams.kandang_id, label: filterParams.kandang_id }
? projectFlockKandangOptions.find(
(opt) => String(opt.value) === filterParams.kandang_id
) || { value: filterParams.kandang_id, label: filterParams.kandang_id }
: null;
const restoredNonstock = filterParams.nonstock_id
? nonstockOptions.find((opt) => String(opt.value) === filterParams.nonstock_id) ||
{ value: filterParams.nonstock_id, label: filterParams.nonstock_id }
? nonstockOptions.find(
(opt) => String(opt.value) === filterParams.nonstock_id
) || {
value: filterParams.nonstock_id,
label: filterParams.nonstock_id,
}
: null;
const restoredCategory = filterParams.category
? categoryOptions.find((opt) => opt.value === filterParams.category) || null
? categoryOptions.find((opt) => opt.value === filterParams.category) ||
null
: null;
formik.setValues({
@@ -731,6 +731,27 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
/>
)}
{!isLoading && data.length > 0 && meta && (
<div className='max-w-sm ml-auto'>
<Pagination
totalItems={meta.total_results || 0}
itemsPerPage={meta.limit || 0}
currentPage={meta.page || 0}
onPrevPage={() =>
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
}
onNextPage={() =>
setCurrentPage((curr) =>
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
)
}
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
rowOptions={[10, 20, 50, 100]}
onRowChange={setPageSize}
/>
</div>
)}
{!isLoading &&
data.length > 0 &&
data.map((customerReport) => {
@@ -663,6 +663,27 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
/>
)}
{!isLoading && data.length > 0 && meta && (
<div className='max-w-sm ml-auto'>
<Pagination
totalItems={meta.total_results || 0}
itemsPerPage={meta.limit || 0}
currentPage={meta.page || 0}
onPrevPage={() =>
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
}
onNextPage={() =>
setCurrentPage((curr) =>
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
)
}
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
rowOptions={[10, 20, 50, 100]}
onRowChange={setPageSize}
/>
</div>
)}
{!isLoading &&
data.length > 0 &&
data.map((supplierReport) => {
@@ -264,20 +264,33 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
handleFilterModalOpenRef.current = () => {
const restoredAreaId = filterParams.area_id
? areaOptions.find((opt) => String(opt.value) === filterParams.area_id) ||
{ value: filterParams.area_id, label: filterParams.area_id }
? areaOptions.find(
(opt) => String(opt.value) === filterParams.area_id
) || { value: filterParams.area_id, label: filterParams.area_id }
: null;
const restoredLocationId = filterParams.location_id
? locationOptions.find((opt) => String(opt.value) === filterParams.location_id) ||
{ value: filterParams.location_id, label: filterParams.location_id }
? locationOptions.find(
(opt) => String(opt.value) === filterParams.location_id
) || {
value: filterParams.location_id,
label: filterParams.location_id,
}
: null;
const restoredProjectFlockId = filterParams.project_flock_id
? projectFlockOptions.find((opt) => String(opt.value) === filterParams.project_flock_id) ||
{ value: filterParams.project_flock_id, label: filterParams.project_flock_id }
? projectFlockOptions.find(
(opt) => String(opt.value) === filterParams.project_flock_id
) || {
value: filterParams.project_flock_id,
label: filterParams.project_flock_id,
}
: null;
const restoredKandangId = filterParams.project_flock_kandang_id
? projectFlockKandangOptions.find((opt) => String(opt.value) === filterParams.project_flock_kandang_id) ||
{ value: filterParams.project_flock_kandang_id, label: filterParams.project_flock_kandang_id }
? projectFlockKandangOptions.find(
(opt) => String(opt.value) === filterParams.project_flock_kandang_id
) || {
value: filterParams.project_flock_kandang_id,
label: filterParams.project_flock_kandang_id,
}
: null;
formik.setValues({
@@ -880,9 +880,9 @@ export function DailyChecklistContent() {
setChecklistStatus('SUBMITTED');
const shareToWhatsApp = () => {
const kandangName = kandangOptions.find(
(k) => String(k.value) === kandangId
)?.label || kandangId;
const kandangName =
kandangOptions.find((k) => String(k.value) === kandangId)?.label ||
kandangId;
const statusMsg = getStatusMessage();
const category = selectedCategory || '';
const message = encodeURIComponent(
+1 -1
View File
@@ -38,7 +38,7 @@ export async function httpClient<T, B = unknown>(
method: opts.method ?? 'GET',
params: opts.query,
data: opts.body,
timeout: opts.timeoutMs ?? 10_000,
timeout: opts.timeoutMs ?? 30_000,
withCredentials: isCookieAuth && !isBearerAuth,
responseType: opts.responseType,
headers: {
+1
View File
@@ -51,6 +51,7 @@ export type CreateProjectFlockPayload = {
category: string;
production_standard_id: number;
location_id: number;
period: number;
kandang_ids: number[];
project_budgets?: ProjectFlockBudget[];
};