Merge branch 'feat/stock-log-export' into 'development'

[FEAT/FE] Stock Log Export

See merge request mbugroup/lti-web-client!466
This commit is contained in:
Rivaldi A N S
2026-05-08 11:59:06 +00:00
9 changed files with 129 additions and 32 deletions
@@ -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,11 +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 { StockLogApi } from '@/services/api/inventory';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { StockLog } from '@/types/api/inventory/product';
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 stockLogTableColumns: ColumnDef<StockLog>[] = [
const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
warehouseName
) => [
{
header: 'ID',
accessorKey: 'id',
@@ -20,6 +29,7 @@ const stockLogTableColumns: ColumnDef<StockLog>[] = [
{
header: 'Gudang',
accessorKey: 'warehouse_name',
cell: warehouseName,
},
{
header: 'Stock Akhir',
@@ -65,31 +75,77 @@ const stockLogTableColumns: ColumnDef<StockLog>[] = [
];
const StockLogTable = ({
stockLogs,
productWarehouse,
}: {
stockLogs: (StockLog & { warehouse_name: string; warehouse_id: number })[];
productWarehouse: ProductWarehouseStock;
}) => {
const { state: tableFilterState, setPage, setPageSize } = useTableFilter();
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'
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
collapsible
variant='bordered'
className={{
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}
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
page={tableFilterState.page ?? 0}
pageSize={tableFilterState.pageSize}
onPageChange={setPage}
onPageSizeChange={setPageSize}
totalItems={stockLogs?.length ?? 0}
isLoading={isLoadingStockLogs}
totalItems={
isResponseSuccess(stockLogsResponse)
? stockLogsResponse.meta?.total_results
: 0
}
className={{
containerClassName: 'mt-6',
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',
@@ -55,7 +55,7 @@ const StockProductWarehouseTable = ({
onPageChange={setPage}
onPageSizeChange={setPageSize}
className={{
containerClassName: 'mt-6',
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',
@@ -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]