Merge branch 'development' into 'production'

Development

See merge request mbugroup/lti-web-client!468
This commit is contained in:
Giovanni Gabriel Septriadi
2026-05-11 08:32:23 +00:00
21 changed files with 608 additions and 518 deletions
+21 -2
View File
@@ -30,6 +30,10 @@ default:
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
- echo "NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV"
- echo "NEXT_PUBLIC_HELPDESK_URL=$NEXT_PUBLIC_HELPDESK_URL"
- echo "NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL=$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
- echo "NEXT_PUBLIC_S3_PUBLIC_BASE_URL=$NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
- echo "Building Next.js static export..."
- npx next build
- |
@@ -41,7 +45,11 @@ default:
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL"
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_APP_ENV": "$NEXT_PUBLIC_APP_ENV",
"NEXT_PUBLIC_HELPDESK_URL": "$NEXT_PUBLIC_HELPDESK_URL",
"NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL": "$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
"NEXT_PUBLIC_S3_PUBLIC_BASE_URL": "NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
}
EOF
artifacts:
@@ -142,6 +150,10 @@ build:dev:
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
NEXT_PUBLIC_APP_ENV: 'development'
NEXT_PUBLIC_HELPDESK_URL: 'https://dev-helpdesk.mbugroup.id/'
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dev-dashboard-ho.mbugroup.id/'
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com'
deploy:dev:
<<: *deploy_template
@@ -170,6 +182,9 @@ build:staging:
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
NEXT_PUBLIC_APP_ENV: 'staging'
NEXT_PUBLIC_HELPDESK_URL: 'https://stg-helpdesk.mbugroup.id/'
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://stg-dashboard-ho.mbugroup.id/'
deploy:staging:
<<: *deploy_template
@@ -185,7 +200,7 @@ deploy:staging:
url: https://stg-lti-erp.mbugroup.id
# ==========================================================
# ====== STAGING (Branch production) ======
# ====== (Branch production) ======
# ==========================================================
build:production:
<<: *build_template
@@ -198,6 +213,10 @@ build:production:
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
NEXT_PUBLIC_APP_ENV: 'production'
NEXT_PUBLIC_HELPDESK_URL: 'https://helpdesk.mbugroup.id/'
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dashboard-ho.mbugroup.id/'
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com/'
deploy:production:
<<: *deploy_template
+41
View File
@@ -161,6 +161,47 @@ const handleFilterLocationChange = useCallback(
- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/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:**
1. **Local sort state** — `const [sorting, setSorting] = useState<SortingState>([]);`
2. **`useTableFilter` config** — Add `sort_by` and `order_by` to `initial` and `paramMap`. The `paramMap` key is the internal name; the value is the query param name sent to the server (they can differ, e.g. `order_by` → `sort_order`):
```ts
initial: { sort_by: '', order_by: '' }
paramMap: { sort_by: 'sort_by', order_by: 'sort_order' }
```
3. **`useEffect` sync** — Watches `sorting` and pushes changes into `useTableFilter`:
```ts
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]);
```
4. **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>`:**
```tsx
<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](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.
+1 -1
View File
@@ -226,7 +226,7 @@ const Pagination = ({
const PageInfo = () => (
<span className='text-nowrap text-sm font-medium text-base-content/50'>
Page {currentPage} of {totalPages}
Total Item: {totalItems} | Page {currentPage} of {totalPages}
</span>
);
@@ -548,21 +548,15 @@ const ExpenseRequestContent = ({
<ul className='list-disc'>
{initialValues?.documents.map(
(requestDocument, requestDocumentIdx) => {
const path = requestDocument.path.startsWith(
'/'
)
? requestDocument.path.slice(1)
: requestDocument.path;
const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
return (
<li key={requestDocumentIdx}>
<Link
href={documentUrl}
href={requestDocument.path}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{requestDocument.path}{' '}
{requestDocument.name}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
@@ -14,18 +14,7 @@ export type ExpensesFilterType = {
export const ExpensesFilterSchema = yup.object({
transaction_date: yup.string().nullable(),
realization_date: yup
.string()
.nullable()
.test(
'is-greater-or-equal-transaction',
'Tanggal realisasi tidak boleh sebelum tanggal transaksi',
function (value) {
const { transaction_date } = this.parent;
if (!transaction_date || !value) return true;
return new Date(value) >= new Date(transaction_date);
}
),
realization_date: yup.string().nullable(),
location: yup
.object({
value: yup.number().required(),
@@ -1,5 +1,6 @@
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import RequirePermission from '@/components/helper/RequirePermission';
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
import { formatCurrency, formatNumber } from '@/lib/helper';
@@ -11,18 +12,6 @@ const InventoryProductDetail = ({
}: {
inventoryProduct?: InventoryProduct;
}) => {
const stockLogs = useMemo(() => {
return (
inventoryProduct?.product_warehouses?.flatMap((warehouse) =>
warehouse.stock_logs.map((log) => ({
...log,
warehouse_name: warehouse.warehouse_name,
warehouse_id: warehouse.warehouse_id,
}))
) || []
);
}, [inventoryProduct]);
return (
<div className='flex flex-col gap-4 p-4'>
<FormHeader
@@ -114,7 +103,14 @@ const InventoryProductDetail = ({
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
/>
<StockLogTable stockLogs={stockLogs} />
<RequirePermission permissions={'lti.inventory.stock_log.list'}>
{inventoryProduct?.product_warehouses?.map((productWarehouse) => (
<StockLogTable
key={productWarehouse.id}
productWarehouse={productWarehouse}
/>
))}
</RequirePermission>
</div>
);
};
@@ -1,25 +1,20 @@
import Button from '@/components/Button';
import Card from '@/components/Card';
import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
import { StockLog } from '@/types/api/inventory/product';
import { StockLogApi } from '@/services/api/inventory';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ProductWarehouseStock, StockLog } from '@/types/api/inventory/product';
import { ColumnDef } from '@tanstack/react-table';
import { FileDown } from 'lucide-react';
import toast from 'react-hot-toast';
import { useState } from 'react';
import useSWR from 'swr';
const StockLogTable = ({
stockLogs,
}: {
stockLogs: (StockLog & { warehouse_name: string; warehouse_id: number })[];
}) => {
return (
<Card
title='Informasi Stock Produk'
collapsible
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<Table<StockLog>
data={stockLogs}
columns={[
const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
warehouseName
) => [
{
header: 'ID',
accessorKey: 'id',
@@ -34,6 +29,7 @@ const StockLogTable = ({
{
header: 'Gudang',
accessorKey: 'warehouse_name',
cell: warehouseName,
},
{
header: 'Stock Akhir',
@@ -76,9 +72,80 @@ const StockLogTable = ({
header: 'Oleh',
accessorKey: 'created_user.name',
},
]}
];
const StockLogTable = ({
productWarehouse,
}: {
productWarehouse: ProductWarehouseStock;
}) => {
const [isExportLoading, setIsExportLoading] = useState(false);
const {
state: tableFilterState,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
product_warehouse_id: productWarehouse.id,
},
});
const handleExportExcel = async () => {
setIsExportLoading(true);
try {
await StockLogApi.exportToExcel(
productWarehouse.warehouse_name,
getTableFilterQueryString()
);
toast.success('Excel berhasil dibuat dan diunduh.');
} catch {
toast.error('Gagal membuat Excel. Silakan coba lagi.');
} finally {
setIsExportLoading(false);
}
};
const { data: stockLogsResponse, isLoading: isLoadingStockLogs } = useSWR(
`${StockLogApi.basePath}${getTableFilterQueryString()}`,
StockLogApi.getAllFetcher
);
const stockLogs = isResponseSuccess(stockLogsResponse)
? stockLogsResponse.data
: [];
return (
<Card
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
collapsible
variant='bordered'
className={{
containerClassName: 'mt-6',
wrapper: 'w-full',
}}
>
<div className='flex justify-end px-6 pt-4'>
<Button onClick={handleExportExcel} isLoading={isExportLoading}>
<FileDown size={16} />
Export Excel
</Button>
</div>
<Table<StockLog>
data={stockLogs}
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
page={tableFilterState.page ?? 0}
pageSize={tableFilterState.pageSize}
onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoadingStockLogs}
totalItems={
isResponseSuccess(stockLogsResponse)
? stockLogsResponse.meta?.total_results
: 0
}
className={{
containerClassName: 'mt-4 mb-0',
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
@@ -1,25 +1,11 @@
import Card from '@/components/Card';
import Table from '@/components/Table';
import { formatNumber } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ProductWarehouseStock } from '@/types/api/inventory/product';
import { ColumnDef } from '@tanstack/react-table';
const StockProductWarehouseTable = ({
productWarehouseStock,
}: {
productWarehouseStock?: ProductWarehouseStock[];
}) => {
return (
<Card
title='Informasi Gudang'
collapsible
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<Table<ProductWarehouseStock>
data={productWarehouseStock ?? []}
columns={[
const stockProductWarehouseTableColumns: ColumnDef<ProductWarehouseStock>[] = [
{
header: 'Nama Gudang',
accessorKey: 'warehouse_name',
@@ -42,9 +28,34 @@ const StockProductWarehouseTable = ({
return formatNumber(props.row.original.current_stock);
},
},
]}
];
const StockProductWarehouseTable = ({
productWarehouseStock,
}: {
productWarehouseStock?: ProductWarehouseStock[];
}) => {
const { state: tableFilterState, setPage, setPageSize } = useTableFilter();
return (
<Card
title='Informasi Gudang'
collapsible
variant='bordered'
className={{
containerClassName: 'mt-6',
wrapper: 'w-full',
}}
>
<Table<ProductWarehouseStock>
data={productWarehouseStock ?? []}
columns={stockProductWarehouseTableColumns}
pageSize={tableFilterState.pageSize}
page={tableFilterState.page ?? 0}
totalItems={productWarehouseStock?.length ?? 0}
onPageChange={setPage}
onPageSizeChange={setPageSize}
className={{
containerClassName: 'mt-6 mb-0',
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
@@ -1,6 +1,5 @@
'use client';
import axios from 'axios';
import Button from '@/components/Button';
import CheckboxInput from '@/components/input/CheckboxInput';
import DateInput from '@/components/input/DateInput';
@@ -27,7 +26,13 @@ import {
MarketingFilter,
} from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { CellContext, ColumnDef, Row } from '@tanstack/react-table';
import {
CellContext,
ColumnDef,
Row,
SortingState,
Updater,
} from '@tanstack/react-table';
import { useRouter } from 'next/navigation';
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
@@ -198,6 +203,8 @@ const MarketingTable = () => {
project_flock_name: '',
project_flock_kandang_id: '',
project_flock_kandang_name: '',
sort_by: '',
order_by: '',
},
paramMap: {
page: 'page',
@@ -207,6 +214,8 @@ const MarketingTable = () => {
customer_id: 'customer_id',
project_flock_id: 'project_flock_id',
project_flock_kandang_id: 'project_flock_kandang_id',
sort_by: 'sort_by',
order_by: 'sort_order',
},
excludeKeysFromUrl: [
'product_names',
@@ -220,6 +229,26 @@ const MarketingTable = () => {
storeName: 'marketing-table',
});
const sorting: SortingState = tableFilterState.sort_by
? [
{
id: tableFilterState.sort_by,
desc: tableFilterState.order_by === 'desc',
},
]
: [];
const handleSortingChange = (updater: Updater<SortingState>) => {
const next = typeof updater === 'function' ? updater(sorting) : updater;
if (next.length > 0) {
updateFilter('sort_by', next[0].id, true);
updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true);
} else {
updateFilter('sort_by', '', true);
updateFilter('order_by', '', true);
}
};
// ===== FETCH DATA =====
const {
data: marketing,
@@ -359,7 +388,6 @@ const MarketingTable = () => {
? 'DELIVERY_ORDER'
: null;
const marketingFilterInitialValues = useMemo(() => {
const productIds = tableFilterState.product_ids
? tableFilterState.product_ids
.split(',')
@@ -374,7 +402,7 @@ const MarketingTable = () => {
.filter(Boolean)
: [];
return {
const marketingFilterInitialValues = {
product_ids: productIds.map((value, idx) => ({
value: Number(value),
label: productLabels[idx] || '-',
@@ -385,21 +413,18 @@ const MarketingTable = () => {
label: tableFilterState.status_name,
}
: null,
customer: tableFilterState.customer_id
? {
value: Number(tableFilterState.customer_id),
label: tableFilterState.customer_name,
}
: null,
project_flock: tableFilterState.project_flock_id
? {
value: Number(tableFilterState.project_flock_id),
label: tableFilterState.project_flock_name,
}
: null,
project_flock_kandang: tableFilterState.project_flock_kandang_id
? {
value: Number(tableFilterState.project_flock_kandang_id),
@@ -407,7 +432,6 @@ const MarketingTable = () => {
}
: null,
};
}, [tableFilterState]);
const approveMarketingHandler = async (notes: string) => {
if (idsToProcess.length === 0) {
@@ -542,27 +566,29 @@ const MarketingTable = () => {
setIsLoadingExportingToExcel(false);
};
const resetExportProgressForm = useCallback(() => {
const resetExportProgressForm = () => {
setExportProgressStartDate('');
setExportProgressEndDate('');
}, []);
};
const exportProgressStartDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
useCallback((e) => {
const exportProgressStartDateChangeHandler: ChangeEventHandler<
HTMLInputElement
> = (e) => {
setExportProgressStartDate(e.target.value);
}, []);
};
const exportProgressEndDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
useCallback((e) => {
const exportProgressEndDateChangeHandler: ChangeEventHandler<
HTMLInputElement
> = (e) => {
setExportProgressEndDate(e.target.value);
}, []);
};
const exportProgressInputToExcelClickHandler = useCallback(() => {
const exportProgressInputToExcelClickHandler = () => {
resetExportProgressForm();
exportProgressInputModal.openModal();
}, [exportProgressInputModal, resetExportProgressForm]);
};
const submitExportProgressInputHandler = useCallback(async () => {
const submitExportProgressInputHandler = async () => {
if (!exportProgressStartDate || !exportProgressEndDate) {
return;
}
@@ -585,12 +611,7 @@ const MarketingTable = () => {
} finally {
setIsExportProgressLoading(false);
}
}, [
exportProgressEndDate,
exportProgressInputModal,
exportProgressStartDate,
resetExportProgressForm,
]);
};
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
return [
@@ -656,7 +677,7 @@ const MarketingTable = () => {
},
},
{
accessorKey: 'so_do_number',
accessorKey: 'so_number',
header: 'No. Order',
cell: (props) => {
return props.row.original.do_number
@@ -672,7 +693,7 @@ const MarketingTable = () => {
},
},
{
accessorKey: 'approval.step_name',
accessorKey: 'status',
header: 'Status',
cell: (props) => {
const approval = props.row.original.latest_approval;
@@ -707,10 +728,12 @@ const MarketingTable = () => {
},
},
{
accessorKey: 'customer.name',
accessorKey: 'customer',
header: 'Customer',
cell: (props) => props.row.original.customer.name,
},
{
accessorKey: 'grand_total',
accessorFn: (row) =>
row.sales_order
?.map((product) => product.total_price)
@@ -727,6 +750,7 @@ const MarketingTable = () => {
{
accessorKey: 'marketing_products.length',
header: 'Product Details',
enableSorting: false,
cell: (props) => {
if (props?.row?.original?.sales_order?.length) {
if (props?.row?.original?.sales_order?.length > 1) {
@@ -859,6 +883,8 @@ const MarketingTable = () => {
'customer_name',
'project_flock_name',
'project_flock_kandang_name',
'sort_by',
'order_by',
]}
onClick={() => {
filterModal.openModal();
@@ -949,6 +975,9 @@ const MarketingTable = () => {
columns={columns}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
sorting={sorting}
setSorting={handleSortingChange}
manualSorting
totalItems={
isResponseSuccess(marketing)
? marketing?.meta?.total_results
@@ -1,13 +1,6 @@
'use client';
import axios from 'axios';
import React, {
useCallback,
useState,
useMemo,
useEffect,
useRef,
} from 'react';
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
@@ -46,8 +39,6 @@ import { useTableFilter } from '@/services/hooks/useTableFilter';
import toast from 'react-hot-toast';
import StatusBadge from '@/components/helper/StatusBadge';
import CheckboxInput from '@/components/input/CheckboxInput';
import { useUiStore } from '@/stores/ui/ui.store';
import { usePathname } from 'next/navigation';
import { Color } from '@/types/theme';
import ButtonFilter from '@/components/helper/ButtonFilter';
import Dropdown from '@/components/Dropdown';
@@ -77,6 +68,26 @@ const getStatusBadgeColor = (status: string): Color => {
return statusBadgeColorMap[normalizedStatus] || 'neutral';
};
const isRecordingApproved = (recording: Recording): boolean => {
return (
recording.approval?.action === 'APPROVED' &&
recording.approval?.step_name === 'Disetujui'
);
};
// ===== FILTER HELPERS =====
const recordingApprovalStatusOptions: OptionType<string>[] = [
{ value: 'CREATED', label: 'Pengajuan' },
{ value: 'UPDATED', label: 'Diperbarui' },
{ value: 'APPROVED', label: 'Disetujui' },
{ value: 'REJECTED', label: 'Ditolak' },
];
const projectFlockCategoryOptions: OptionType<string>[] = [
{ value: 'GROWING', label: 'Growing' },
{ value: 'LAYING', label: 'Laying' },
];
const RowOptionsMenu = ({
popoverPosition = 'bottom',
props,
@@ -268,25 +279,31 @@ const RowOptionsMenu = ({
};
const RecordingTable = () => {
const { searchValue, setSearchValue, setTableState } = useUiStore();
const pathname = usePathname();
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
} = useTableFilter<{
search: string;
areaFilter: OptionType<number> | null;
locationFilter: OptionType<number> | null;
projectFlockFilter: OptionType<number> | null;
kandangFilter: OptionType<number> | null;
projectFlockKandangFilter: number | null;
approvalStatusFilter: OptionType<string> | null;
projectFlockCategoryFilter: OptionType<string> | null;
}>({
initial: {
search: '',
areaFilter: '',
locationFilter: '',
projectFlockFilter: '',
kandangFilter: '',
projectFlockKandangFilter: '',
approvalStatusFilter: '',
projectFlockCategoryFilter: '',
areaFilter: null,
locationFilter: null,
projectFlockFilter: null,
kandangFilter: null,
projectFlockKandangFilter: null,
approvalStatusFilter: null,
projectFlockCategoryFilter: null,
},
paramMap: {
page: 'page',
@@ -300,31 +317,58 @@ const RecordingTable = () => {
approvalStatusFilter: 'approval_status',
projectFlockCategoryFilter: 'project_flock_category',
},
});
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
persist: true,
storeName: 'recording-table',
});
// ===== FILTER MODAL STATE =====
const filterModal = useModal();
// ===== FILTER STATE =====
const [filterArea, setFilterArea] = useState<OptionType | null>(null);
const [filterLocation, setFilterLocation] = useState<OptionType | null>(null);
const [filterProjectFlock, setFilterProjectFlock] =
useState<OptionType | null>(null);
const [filterKandang, setFilterKandang] = useState<OptionType | null>(null);
const [, setFilterProjectFlockKandangId] = useState<number | undefined>(
undefined
);
const [filterLocationAreaId, setFilterLocationAreaId] = useState<string>('');
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
useState<string>('');
// ===== FORMIK SETUP =====
const formik = useFormik<RecordingFilterType>({
initialValues: {
area_id: tableFilterState.areaFilter,
location_id: tableFilterState.locationFilter,
project_flock_id: tableFilterState.projectFlockFilter,
kandang_id: tableFilterState.kandangFilter,
project_flock_kandang_id: tableFilterState.projectFlockKandangFilter,
approval_status: tableFilterState.approvalStatusFilter,
project_flock_category: tableFilterState.projectFlockCategoryFilter,
},
validationSchema: RecordingFilterSchema,
onSubmit: (values, { setSubmitting }) => {
updateFilter('areaFilter', values.area_id, true);
updateFilter('locationFilter', values.location_id, true);
updateFilter('projectFlockFilter', values.project_flock_id, true);
updateFilter('kandangFilter', values.kandang_id, true);
updateFilter(
'projectFlockKandangFilter',
values.project_flock_kandang_id,
true
);
updateFilter('approvalStatusFilter', values.approval_status, true);
updateFilter(
'projectFlockCategoryFilter',
values.project_flock_category,
true
);
filterModal.closeModal();
setSubmitting(false);
},
});
const formikResetHandler = () => {
updateFilter('areaFilter', null, true);
updateFilter('locationFilter', null, true);
updateFilter('projectFlockFilter', null, true);
updateFilter('kandangFilter', null, true);
updateFilter('projectFlockKandangFilter', null, true);
updateFilter('approvalStatusFilter', null, true);
updateFilter('projectFlockCategoryFilter', null, true);
formik.resetForm({
values: {
area_id: null,
location_id: null,
project_flock_id: null,
@@ -333,35 +377,13 @@ const RecordingTable = () => {
approval_status: null,
project_flock_category: null,
},
validationSchema: RecordingFilterSchema,
onSubmit: (values, { setSubmitting }) => {
updateFilter('areaFilter', values.area_id || '');
updateFilter('locationFilter', values.location_id || '');
updateFilter('projectFlockFilter', values.project_flock_id || '');
updateFilter('kandangFilter', values.kandang_id || '');
updateFilter(
'projectFlockKandangFilter',
values.project_flock_kandang_id || ''
);
updateFilter('approvalStatusFilter', values.approval_status || '');
updateFilter(
'projectFlockCategoryFilter',
values.project_flock_category || ''
);
filterModal.closeModal();
setSubmitting(false);
},
onReset: () => {
updateFilter('areaFilter', '');
updateFilter('locationFilter', '');
updateFilter('projectFlockFilter', '');
updateFilter('kandangFilter', '');
updateFilter('projectFlockKandangFilter', '');
updateFilter('approvalStatusFilter', '');
updateFilter('projectFlockCategoryFilter', '');
},
});
filterModal.closeModal();
};
const { project_flock_id, kandang_id } = formik.values;
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection).map((item) =>
@@ -396,13 +418,6 @@ const RecordingTable = () => {
);
// ===== LOCATION, AREA, KANDANG OPTIONS =====
const locationParams = useMemo(() => {
if (filterLocationAreaId) {
return { area_id: filterLocationAreaId };
}
return undefined;
}, [filterLocationAreaId]);
const {
setInputValue: setLocationInputValue,
options: locationOptions,
@@ -413,7 +428,9 @@ const RecordingTable = () => {
'id',
'name',
'search',
locationParams
{
area_id: String(formik.values.area_id?.value),
}
);
const {
@@ -428,13 +445,6 @@ const RecordingTable = () => {
'search'
);
const projectFlockParams = useMemo(() => {
if (filterProjectFlockLocationId) {
return { location_id: filterProjectFlockLocationId };
}
return undefined;
}, [filterProjectFlockLocationId]);
const {
setInputValue: setProjectFlockInputValue,
options: projectFlockOptions,
@@ -446,34 +456,41 @@ const RecordingTable = () => {
'id',
'flock_name',
'search',
projectFlockParams
{
location_id: String(formik.values.location_id?.value),
}
);
const kandangOptions = useMemo(() => {
if (!filterProjectFlock || !projectFlocksRawData) return [];
if (!project_flock_id || !projectFlocksRawData) return [];
if (!isResponseSuccess(projectFlocksRawData)) return [];
const data = projectFlocksRawData.data as ProjectFlock[];
const selectedProjectFlockData = data.find(
(pf) => pf.id === filterProjectFlock.value
const selectedProjectFlockData = data.find((pf) =>
pf.id === formik.values.project_flock_id?.value
? Number(formik.values.project_flock_id.value)
: 0
);
if (!selectedProjectFlockData?.kandangs) return [];
return selectedProjectFlockData.kandangs.map((k) => ({
value: k.id,
label: k.name || '',
}));
}, [filterProjectFlock, projectFlocksRawData]);
}, [project_flock_id, projectFlocksRawData]);
// ===== PROJECT FLOCK KANDANG LOOKUP =====
const projectFlockKandangLookupUrl = useMemo(() => {
if (!filterProjectFlock || !filterKandang) return null;
if (!project_flock_id?.value || !kandang_id?.value) return null;
const params = new URLSearchParams({
project_flock_id: filterProjectFlock.value.toString(),
kandang_id: filterKandang.value.toString(),
project_flock_id: project_flock_id.value.toString(),
kandang_id: kandang_id.value.toString(),
});
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
}, [filterProjectFlock, filterKandang]);
}, [project_flock_id, kandang_id]);
const { data: projectFlockKandangLookupData } = useSWR(
projectFlockKandangLookupUrl,
@@ -495,154 +512,45 @@ const RecordingTable = () => {
? projectFlockKandangLookupData.data
: undefined;
const formikRef = useRef(formik);
useEffect(() => {
formikRef.current = formik;
});
useEffect(() => {
if (projectFlockKandangLookup?.id) {
const pfkId = String(projectFlockKandangLookup.id);
setFilterProjectFlockKandangId(projectFlockKandangLookup.id);
formikRef.current.setFieldValue('project_flock_kandang_id', pfkId);
formik.setFieldValue('project_flock_kandang_id', pfkId);
} else {
setFilterProjectFlockKandangId(undefined);
formikRef.current.setFieldValue('project_flock_kandang_id', null);
formik.setFieldValue('project_flock_kandang_id', null);
}
}, [projectFlockKandangLookup]);
// ===== FILTER HANDLERS =====
const handleFilterAreaChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const area = val as OptionType | null;
const areaId = area?.value ? String(area.value) : null;
formik.setFieldValue('area_id', areaId);
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('area_id', val);
formik.setFieldValue('location_id', null);
formik.setFieldValue('project_flock_id', null);
formik.setFieldValue('kandang_id', null);
formik.setFieldValue('project_flock_kandang_id', null);
};
setFilterArea(area);
setFilterLocation(null);
setFilterProjectFlock(null);
setFilterKandang(null);
setFilterLocationAreaId(areaId || '');
setFilterProjectFlockLocationId('');
},
[formik]
);
const handleFilterLocationChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const location = val as OptionType | null;
const locationId = location?.value ? String(location.value) : null;
formik.setFieldValue('location_id', locationId);
const handleFilterLocationChange = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('location_id', val);
formik.setFieldValue('project_flock_id', null);
formik.setFieldValue('kandang_id', null);
formik.setFieldValue('project_flock_kandang_id', null);
};
setFilterLocation(location);
setFilterProjectFlock(null);
setFilterKandang(null);
setFilterProjectFlockLocationId(locationId || '');
},
[formik]
);
const handleFilterProjectFlockChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const projectFlock = val as OptionType | null;
const projectFlockId = projectFlock?.value
? String(projectFlock.value)
: null;
formik.setFieldValue('project_flock_id', projectFlockId);
const handleFilterProjectFlockChange = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('project_flock_id', val);
formik.setFieldValue('kandang_id', null);
formik.setFieldValue('project_flock_kandang_id', null);
};
setFilterProjectFlock(projectFlock);
setFilterKandang(null);
},
[formik]
);
const handleFilterKandangChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const kandang = val as OptionType | null;
const kandangId = kandang?.value ? String(kandang.value) : null;
formik.setFieldValue('kandang_id', kandangId);
const handleFilterKandangChange = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('kandang_id', val);
formik.setFieldValue('project_flock_kandang_id', null);
setFilterKandang(kandang);
},
[formik]
);
// ===== FILTER HELPERS =====
const areaIdValue = useMemo(() => {
if (!formik.values.area_id) return null;
return (
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
null
);
}, [formik.values.area_id, areaOptions]);
const locationIdValue = useMemo(() => {
if (!formik.values.location_id) return null;
return (
locationOptions.find(
(opt) => String(opt.value) === formik.values.location_id
) || null
);
}, [formik.values.location_id, locationOptions]);
const projectFlockIdValue = useMemo(() => {
if (!filterProjectFlock) return null;
return filterProjectFlock;
}, [filterProjectFlock]);
const kandangIdValue = useMemo(() => {
if (!formik.values.kandang_id) return null;
return (
kandangOptions.find(
(opt) => String(opt.value) === formik.values.kandang_id
) || null
);
}, [formik.values.kandang_id, kandangOptions]);
const recordingApprovalStatusOptions: OptionType<string>[] = [
{ value: 'CREATED', label: 'Pengajuan' },
{ value: 'UPDATED', label: 'Diperbarui' },
{ value: 'APPROVED', label: 'Disetujui' },
{ value: 'REJECTED', label: 'Ditolak' },
];
const projectFlockCategoryOptions: OptionType<string>[] = [
{ value: 'GROWING', label: 'Growing' },
{ value: 'LAYING', label: 'Laying' },
];
const approvalStatusValue = useMemo(() => {
if (!formik.values.approval_status) return null;
return (
recordingApprovalStatusOptions.find(
(opt) => opt.value === formik.values.approval_status
) || null
);
}, [formik.values.approval_status]);
const projectFlockCategoryValue = useMemo(() => {
if (!formik.values.project_flock_category) return null;
return (
projectFlockCategoryOptions.find(
(opt) => opt.value === formik.values.project_flock_category
) || null
);
}, [formik.values.project_flock_category]);
};
// ===== HANDLE FILTER MODAL OPEN =====
const handleFilterModalOpen = () => {
@@ -650,25 +558,9 @@ const RecordingTable = () => {
formik.validateForm();
};
const isRecordingApproved = useCallback((recording: Recording): boolean => {
return (
recording.approval?.action === 'APPROVED' &&
recording.approval?.step_name === 'Disetujui'
);
}, []);
useEffect(() => {
setTableState('recording-table', pathname);
}, [pathname, setTableState]);
const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value);
setSearchValue(e.target.value);
setPage(1);
},
[updateFilter, setSearchValue, setPage]
);
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value, true);
};
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
@@ -1220,7 +1112,7 @@ const RecordingTable = () => {
return (
<div className='text-center'>
{value !== null && value !== undefined
? `${value.toFixed(2)}%`
? `${value.toFixed(2)} butir`
: '-'}
</div>
);
@@ -1236,7 +1128,7 @@ const RecordingTable = () => {
return (
<div className='text-center text-gray-600'>
{value !== null && value !== undefined
? `${value.toFixed(2)}%`
? `${value.toFixed(2)} btr`
: '-'}
</div>
);
@@ -1572,13 +1464,13 @@ const RecordingTable = () => {
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
<div className='p-4 flex flex-col gap-1.5'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={areaOptions}
value={areaIdValue}
value={formik.values.area_id}
onChange={handleFilterAreaChange}
onInputChange={setAreaInputValue}
isLoading={isLoadingAreaOptions}
@@ -1591,13 +1483,13 @@ const RecordingTable = () => {
label='Lokasi'
placeholder='Pilih Lokasi'
options={locationOptions}
value={locationIdValue}
value={formik.values.location_id}
onChange={handleFilterLocationChange}
onInputChange={setLocationInputValue}
isLoading={isLoadingLocationOptions}
isClearable
onMenuScrollToBottom={loadMoreLocations}
isDisabled={!filterArea}
isDisabled={!formik.values.area_id?.value}
className={{ wrapper: 'w-full' }}
/>
@@ -1605,13 +1497,13 @@ const RecordingTable = () => {
label='Project Flock'
placeholder='Pilih Project Flock'
options={projectFlockOptions}
value={projectFlockIdValue}
value={formik.values.project_flock_id}
onChange={handleFilterProjectFlockChange}
onInputChange={setProjectFlockInputValue}
isLoading={isLoadingProjectFlocks}
isClearable
onMenuScrollToBottom={loadMoreProjectFlocks}
isDisabled={!filterLocation}
isDisabled={!formik.values.location_id?.value}
className={{ wrapper: 'w-full' }}
/>
@@ -1619,11 +1511,11 @@ const RecordingTable = () => {
label='Kandang'
placeholder='Pilih Kandang'
options={kandangOptions}
value={kandangIdValue}
value={formik.values.kandang_id}
onChange={handleFilterKandangChange}
isLoading={!filterProjectFlock}
isLoading={!formik.values.project_flock_id?.value}
isClearable
isDisabled={!filterProjectFlock}
isDisabled={!formik.values.project_flock_id?.value}
className={{ wrapper: 'w-full' }}
/>
@@ -1631,12 +1523,9 @@ const RecordingTable = () => {
label='Kategori'
placeholder='Pilih Kategori'
options={projectFlockCategoryOptions}
value={projectFlockCategoryValue}
value={formik.values.project_flock_category}
onChange={(val) => {
formik.setFieldValue(
'project_flock_category',
!Array.isArray(val) && val ? String(val.value) : null
);
formik.setFieldValue('project_flock_category', val);
}}
isClearable
className={{ wrapper: 'w-full' }}
@@ -1646,12 +1535,9 @@ const RecordingTable = () => {
label='Status Approval'
placeholder='Pilih Status Approval'
options={recordingApprovalStatusOptions}
value={approvalStatusValue}
value={formik.values.approval_status}
onChange={(val) => {
formik.setFieldValue(
'approval_status',
!Array.isArray(val) && val ? String(val.value) : null
);
formik.setFieldValue('approval_status', val);
}}
isClearable
className={{ wrapper: 'w-full' }}
@@ -1661,19 +1547,9 @@ const RecordingTable = () => {
{/* Modal Footer */}
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button
type='button'
type='reset'
variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
onClick={() => {
formik.resetForm();
setFilterArea(null);
setFilterLocation(null);
setFilterProjectFlock(null);
setFilterKandang(null);
setFilterLocationAreaId('');
setFilterProjectFlockLocationId('');
filterModal.closeModal();
}}
>
Reset Filter
</Button>
@@ -1,21 +1,40 @@
import { string, object } from 'yup';
import { OptionType } from '@/components/input/SelectInput';
import * as Yup from 'yup';
export const RecordingFilterSchema = object().shape({
area_id: string().nullable(),
location_id: string().nullable(),
project_flock_id: string().nullable(),
kandang_id: string().nullable(),
project_flock_kandang_id: string().nullable(),
approval_status: string().nullable(),
project_flock_category: string().nullable(),
export const RecordingFilterSchema = Yup.object().shape({
area_id: Yup.object({
value: Yup.number().nullable(),
label: Yup.string().nullable(),
}).nullable(),
location_id: Yup.object({
value: Yup.number().nullable(),
label: Yup.string().nullable(),
}).nullable(),
project_flock_id: Yup.object({
value: Yup.number().nullable(),
label: Yup.string().nullable(),
}).nullable(),
kandang_id: Yup.object({
value: Yup.number().nullable(),
label: Yup.string().nullable(),
}).nullable(),
project_flock_kandang_id: Yup.number().nullable(),
approval_status: Yup.object({
value: Yup.string().nullable(),
label: Yup.string().nullable(),
}).nullable(),
project_flock_category: Yup.object({
value: Yup.string().nullable(),
label: Yup.string().nullable(),
}).nullable(),
});
export type RecordingFilterType = {
area_id: string | null;
location_id: string | null;
project_flock_id: string | null;
kandang_id: string | null;
project_flock_kandang_id: string | null;
approval_status: string | null;
project_flock_category: string | null;
area_id: OptionType<number> | null;
location_id: OptionType<number> | null;
project_flock_id: OptionType<number> | null;
kandang_id: OptionType<number> | null;
project_flock_kandang_id: number | null;
approval_status: OptionType<string> | null;
project_flock_category: OptionType<string> | null;
};
@@ -4,6 +4,7 @@ import {
CreateGrowingRecordingPayload,
CreateLayingRecordingPayload,
CreateEggPayload,
RecordingStock,
} from '@/types/api/production/recording';
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
@@ -42,7 +43,7 @@ type RecordingGrowingFormSchemaType = {
product_warehouse_id?: {
value: number;
label: string;
};
} | null;
source_product_warehouse_id?: number;
qty?: number | string;
}[];
@@ -53,7 +54,7 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
product_warehouse_id?: {
value: number;
label: string;
};
} | null;
qty?: number | string;
weight?: number | string;
}[];
@@ -71,7 +72,7 @@ export type DepletionSchema = {
product_warehouse_id?: {
value: number;
label: string;
};
} | null;
source_product_warehouse_id?: number;
qty?: number | string;
};
@@ -80,7 +81,7 @@ export type EggSchema = {
product_warehouse_id?: {
value: number;
label: string;
};
} | null;
qty?: number | string;
weight?: number | string;
};
@@ -104,7 +105,7 @@ const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
label: Yup.string().required(),
})
.optional()
.typeError('Depletions harus berupa angka!'),
.nullable(),
source_product_warehouse_id: Yup.number()
.optional()
.typeError('Gudang sumber harus berupa angka!'),
@@ -119,7 +120,7 @@ const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
label: Yup.string().required(),
})
.optional()
.typeError('Kondisi telur harus berupa angka!'),
.nullable(),
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
});
@@ -282,8 +283,9 @@ export const getRecordingGrowingFormInitialValues = (
label: getProductWarehouseOptionLabel(stock.product_warehouse),
},
qty:
(stock as { qty?: number; usage_amount?: number }).qty ||
(stock as { qty?: number; usage_amount?: number }).usage_amount ||
(stock as RecordingStock).qty ||
((stock as RecordingStock).usage_amount || 0) +
((stock as RecordingStock).pending_qty || 0) ||
'',
})) ?? [
{
@@ -324,7 +326,7 @@ export const getRecordingLayingFormInitialValues = (
weight: egg.weight,
})) ?? [
{
product_warehouse_id: undefined,
product_warehouse_id: null,
qty: '',
weight: '',
},
@@ -1472,7 +1472,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
(productWarehouseId: number) => {
if ((type === 'edit' || type === 'detail') && initialValues?.stocks) {
const existingStock = initialValues.stocks.find(
(s) => s.product_warehouse_id === productWarehouseId
(s) => Number(s.product_warehouse_id) === Number(productWarehouseId)
) as RecordingStock | undefined;
if (existingStock) {
return {
@@ -1508,9 +1508,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (pendingQty > 0) {
return (
<span className='text-sm text-gray-600 whitespace-nowrap'>
(tersedia: {formatNumber(requestedUsage)} | pending:{' '}
(tersedia: {formatNumber(availableStock)} | pending:{' '}
<span className='text-error'>{formatNumber(pendingQty)}</span> |
pakai: {formatNumber(requestedUsage + pendingQty)})
pakai: {formatNumber(requestedUsage)})
</span>
);
}
@@ -1731,14 +1731,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldTouched('stocks', false, false);
formik.setFieldValue('stocks', [
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
},
]);
formik.setFieldTouched('depletions', false, false);
formik.setFieldValue('depletions', [
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
},
]);
@@ -1746,7 +1746,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldTouched('eggs', false, false);
formik.setFieldValue('eggs', [
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
weight: '',
},
@@ -1795,14 +1795,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldTouched('stocks', false, false);
formik.setFieldValue('stocks', [
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
},
]);
formik.setFieldTouched('depletions', false, false);
formik.setFieldValue('depletions', [
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
},
]);
@@ -1810,7 +1810,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldTouched('eggs', false, false);
formik.setFieldValue('eggs', [
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
weight: '',
},
@@ -1848,14 +1848,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldTouched('stocks', false, false);
formik.setFieldValue('stocks', [
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
},
]);
formik.setFieldTouched('depletions', false, false);
formik.setFieldValue('depletions', [
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
},
]);
@@ -1863,7 +1863,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.setFieldTouched('eggs', false, false);
formik.setFieldValue('eggs', [
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
weight: '',
},
@@ -2076,7 +2076,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newStocks = [
...(formik.values.stocks || []),
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
},
];
@@ -2108,7 +2108,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newDepletions = [
...(formik.values.depletions || []),
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
},
];
@@ -2142,7 +2142,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const newEggs = [
...((formik.values as RecordingLayingFormValues).eggs || []),
{
product_warehouse_id: 0,
product_warehouse_id: null,
qty: '',
},
];
@@ -2185,7 +2185,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') {
const layingValues = formik.values as RecordingLayingFormValues;
if (!layingValues.eggs || layingValues.eggs.length === 0) {
setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]);
setFieldValue('eggs', [{ product_warehouse_id: null, qty: '' }]);
}
}
}, [isLayingCategory, type, formik.values, setFieldValue]);
@@ -732,7 +732,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
)}
{!isLoading && data.length > 0 && meta && (
<div className='max-w-sm ml-auto'>
<div className='w-full ml-auto'>
<Pagination
totalItems={meta.total_results || 0}
itemsPerPage={meta.limit || 0}
@@ -664,7 +664,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
)}
{!isLoading && data.length > 0 && meta && (
<div className='max-w-sm ml-auto'>
<div className='w-full ml-auto'>
<Pagination
totalItems={meta.total_results || 0}
itemsPerPage={meta.limit || 0}
+5 -1
View File
@@ -197,6 +197,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
icon: 'heroicons-outline:folder',
permission: [
'lti.inventory.product_stock.list',
'lti.inventory.stock_log.list',
'lti.inventory.product_warehouses.list',
'lti.inventory.transfer.list',
],
@@ -204,7 +205,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
{
text: 'Stok Produk',
link: '/inventory/product',
permission: ['lti.inventory.product_stock.list'],
permission: [
'lti.inventory.product_stock.list',
'lti.inventory.stock_log.list',
],
},
{
text: 'Penyesuaian Stok',
+41 -1
View File
@@ -13,7 +13,9 @@ import {
CreateInventoryAdjustmentPayload,
InventoryAdjustment,
} from '@/types/api/inventory/adjustment';
import { InventoryProduct } from '@/types/api/inventory/product';
import { InventoryProduct, StockLog } from '@/types/api/inventory/product';
import { httpClient } from '../http/client';
import { formatDate } from '@/lib/helper';
export const ProductWarehouseApi = new BaseApiService<
ProductWarehouse,
@@ -65,3 +67,41 @@ export const InventoryProductApi = new BaseApiService<
unknown,
unknown
>('/inventory/product-stocks');
export class StockLogService extends BaseApiService<
StockLog,
unknown,
unknown
> {
constructor(basePath: string = '/inventory/stock-logs') {
super(basePath);
}
async exportToExcel(warehouseName: string, initialQueryString: string) {
const params = new URLSearchParams(initialQueryString);
params.set('export', 'excel');
params.set('page', '1');
params.set('limit', '99999999999');
const queryString = `?${params.toString()}`;
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
method: 'GET',
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement('a');
link.href = url;
const fileName = `informasi-stok-produk-${warehouseName.toLowerCase().replaceAll(' ', '-')}-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
}
}
export const StockLogApi = new StockLogService('/inventory/stock-logs');
+3 -5
View File
@@ -1,6 +1,5 @@
import { isResponseError } from '@/lib/api-helper';
import { BaseApiService } from '@/services/api/base';
import { httpClient, httpClientFetcher } from '@/services/http/client';
import { httpClient } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import axios from 'axios';
import {
@@ -11,9 +10,8 @@ import {
CreateDeliveryOrderPayload,
UpdateDeliveryOrderPayload,
} from '@/types/api/marketing/marketing';
import toast from 'react-hot-toast';
import * as XLSX from 'xlsx';
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
import { formatDate } from '@/lib/helper';
/**
* 💡 Helper untuk membuat respons dummy
+3
View File
@@ -249,6 +249,9 @@ export function useTableFilter<
const mapKey = useCallback(
(key: string) => {
const m = options?.paramMap as Record<string, string> | undefined;
if (key === 'pageSize' && ((m && !m[key]) || !m)) return 'limit';
return (m && m[key]) || key;
},
[options?.paramMap]
+1
View File
@@ -10,6 +10,7 @@ export type BaseExpense = {
category: 'BOP' | 'NON-BOP';
documents?: {
id: number;
name: string;
path: string;
}[];
realization_docs?: {
+1
View File
@@ -62,6 +62,7 @@ export type RecordingDepletion = {
export type RecordingStock = {
product_warehouse_id: number;
qty?: number;
usage_amount?: number;
pending_qty: number;
product_warehouse: ProductWarehouse;