mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
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:
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user