From e7f378823c53e0cd66c207cb4f1f4c6e009fde39 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 8 May 2026 18:57:21 +0700 Subject: [PATCH 1/5] feat: implement export product stock log --- .../product/detail/InventoryProductDetail.tsx | 22 +++--- .../product/detail/StockLogTable.tsx | 74 ++++++++++++++++--- src/services/api/inventory.ts | 42 ++++++++++- 3 files changed, 115 insertions(+), 23 deletions(-) diff --git a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx index 39609b06..715a7a43 100644 --- a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx +++ b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx @@ -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 (
- + + {inventoryProduct?.product_warehouses?.map((productWarehouse) => ( + + ))} +
); }; diff --git a/src/components/pages/inventory/product/detail/StockLogTable.tsx b/src/components/pages/inventory/product/detail/StockLogTable.tsx index a8240952..b92a4512 100644 --- a/src/components/pages/inventory/product/detail/StockLogTable.tsx +++ b/src/components/pages/inventory/product/detail/StockLogTable.tsx @@ -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[] = [ +const stockLogTableColumns: (warehouseName: string) => ColumnDef[] = ( + warehouseName +) => [ { header: 'ID', accessorKey: 'id', @@ -20,6 +29,7 @@ const stockLogTableColumns: ColumnDef[] = [ { header: 'Gudang', accessorKey: 'warehouse_name', + cell: warehouseName, }, { header: 'Stock Akhir', @@ -65,31 +75,77 @@ const stockLogTableColumns: ColumnDef[] = [ ]; 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 ( +
+ +
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', diff --git a/src/services/api/inventory.ts b/src/services/api/inventory.ts index 70a7c8f9..df72959f 100644 --- a/src/services/api/inventory.ts +++ b/src/services/api/inventory.ts @@ -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(`${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'); From a0e8c600820fb5eff9a8dc84212de6051f18657e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 8 May 2026 18:57:37 +0700 Subject: [PATCH 2/5] chore: adjust styling --- .../inventory/product/detail/StockProductWarehouseTable.tsx | 2 +- src/components/pages/report/finance/tab/CustomerPaymentTab.tsx | 2 +- src/components/pages/report/finance/tab/DebtSupplierTab.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx index 4d361c5c..8b36ee4b 100644 --- a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx +++ b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx @@ -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', diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index ad59cfb0..55cf08f3 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -732,7 +732,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { )} {!isLoading && data.length > 0 && meta && ( -
+
{ )} {!isLoading && data.length > 0 && meta && ( -
+
Date: Fri, 8 May 2026 18:58:02 +0700 Subject: [PATCH 3/5] feat: add stock log permission --- src/config/constant.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/constant.ts b/src/config/constant.ts index 0f06d499..251d10e5 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -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', From 7f9bb8e11de2910f753f5a008b4d2f4938c75352 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 8 May 2026 18:58:13 +0700 Subject: [PATCH 4/5] chore: remove unnecessary code --- src/services/api/marketing/marketing.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/services/api/marketing/marketing.ts b/src/services/api/marketing/marketing.ts index 923f9724..2cd225a5 100644 --- a/src/services/api/marketing/marketing.ts +++ b/src/services/api/marketing/marketing.ts @@ -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 From a9a5098a21f31ae18c6dc75dd5394ce48114e774 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 8 May 2026 18:58:25 +0700 Subject: [PATCH 5/5] fix: set default map for pageSize to limit --- src/services/hooks/useTableFilter.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/hooks/useTableFilter.tsx b/src/services/hooks/useTableFilter.tsx index ad9c6679..acc1dff7 100644 --- a/src/services/hooks/useTableFilter.tsx +++ b/src/services/hooks/useTableFilter.tsx @@ -249,6 +249,9 @@ export function useTableFilter< const mapKey = useCallback( (key: string) => { const m = options?.paramMap as Record | undefined; + + if (key === 'pageSize' && ((m && !m[key]) || !m)) return 'limit'; + return (m && m[key]) || key; }, [options?.paramMap]