From d7384752a099288c93c93b2766ffa873404a58a0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 9 Dec 2025 14:11:15 +0700 Subject: [PATCH 01/56] feat(FE-361): Add Laporan report menu with logistic submenu --- src/config/constant.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index dc36025b..5b4939bc 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -58,6 +58,19 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ icon: 'uil:wallet', }, + { + title: 'Laporan', + link: '/report', + icon: 'mdi:chart-box-outline', + submenu: [ + { + title: 'Logistik & Persediaan', + link: '/report/logistic-stock', + icon: 'mdi:warehouse', + }, + ], + }, + { title: 'Persediaan', link: '/inventory', From b039ec832b191445e35493d8760e39f22a00c697 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 9 Dec 2025 15:49:59 +0700 Subject: [PATCH 02/56] feat(FE-361): Add logistic-stock report page and table footer --- src/app/report/logistic-stock/layout.tsx | 11 + src/app/report/logistic-stock/page.tsx | 22 + src/components/Table.tsx | 40 ++ .../PurchasesPerSupplierTab.tsx | 521 ++++++++++++++++++ 4 files changed, 594 insertions(+) create mode 100644 src/app/report/logistic-stock/layout.tsx create mode 100644 src/app/report/logistic-stock/page.tsx create mode 100644 src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx diff --git a/src/app/report/logistic-stock/layout.tsx b/src/app/report/logistic-stock/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/report/logistic-stock/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/report/logistic-stock/page.tsx b/src/app/report/logistic-stock/page.tsx new file mode 100644 index 00000000..5dce741c --- /dev/null +++ b/src/app/report/logistic-stock/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +import Tabs from '@/components/Tabs'; +import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/PurchasesPerSupplierTab'; + +const LogisticStock = () => { + const tabs = [ + { + id: '1', + label: 'Rekapitulasi Pembelian Per Supplier', + content: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default LogisticStock; diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b02dd3b5..5c76f44e 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -31,6 +31,9 @@ interface TableClassNames { tableBodyClassName?: string; bodyRowClassName?: string; bodyColumnClassName?: string; + tableFooterClassName?: string; + footerRowClassName?: string; + footerColumnClassName?: string; paginationClassName?: string; } @@ -52,6 +55,9 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); + renderFooter?: boolean; + footerContent?: ReactNode; + footerData?: TData[]; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -84,6 +90,9 @@ const Table = ({ tableBodyClassName: '', bodyRowClassName: '', bodyColumnClassName: '', + tableFooterClassName: '', + footerRowClassName: '', + footerColumnClassName: '', paginationClassName: '', }, emptyContent = emptyContentDefaultValue, @@ -93,6 +102,9 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, + renderFooter = false, + footerContent, + footerData = [], }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -160,6 +172,14 @@ const Table = ({ const table = useReactTable(tableOptions); const { setPageSize } = table; + const footerTableOptions: TableOptions = { + columns, + data: footerData, + getCoreRowModel: getCoreRowModel(), + }; + + const footerTable = useReactTable(footerTableOptions); + const prevPageClickHandler = () => { table.previousPage(); @@ -262,6 +282,26 @@ const Table = ({ ))} + + {renderFooter && + (footerData && footerData.length > 0 + ? footerTable.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + : footerContent)} + diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx new file mode 100644 index 00000000..a6f5f364 --- /dev/null +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -0,0 +1,521 @@ +import { useState, useMemo } from 'react'; +import Card from '@/components/Card'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import { AreaApi } from '@/services/api/master-data'; +import { SupplierApi } from '@/services/api/master-data'; +import { ProductApi } from '@/services/api/master-data'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; +import { formatCurrency, formatDate } from '@/lib/helper'; + +const PurchasesPerSupplierTab = () => { + const [selectedArea, setSelectedArea] = useState(null); + const [selectedSupplier, setSelectedSupplier] = useState(null); + const [selectedProduct, setSelectedProduct] = useState(null); + + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, + 'id', + 'name', + 'search' + ); + + const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = + useSelect(SupplierApi.basePath, 'id', 'name', 'search'); + + const { options: productOptions, isLoadingOptions: isLoadingProducts } = + useSelect(ProductApi.basePath, 'id', 'name', 'search'); + + const data = useMemo( + () => [ + { + id: 1, + supplier: 'PT. RAJAWALI MITRA PAKANINDO', + items: [ + { + id: 1, + received_date: '2025-09-26', + po_date: '2025-09-30', + po_number: 'PO-MBU-00670', + product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG', + destination_warehouse: 'GUDANG CIMARAGAS 1', + qty: 5, + price: 45000, + purchase_amount: 225000, + transport: 0, + value_transport: 0, + total: 225000, + expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', + travel_number: '-', + }, + { + id: 2, + received_date: '2025-09-30', + po_date: '2025-09-30', + po_number: 'PO-MBU-00670', + product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG', + destination_warehouse: 'GUDANG CIMARAGAS 2', + qty: 5, + price: 45000, + purchase_amount: 225000, + transport: 0, + value_transport: 0, + total: 225000, + expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', + travel_number: '-', + }, + { + id: 3, + received_date: '2025-09-30', + po_date: '2025-09-30', + po_number: 'PO-MBU-00670', + product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG', + destination_warehouse: 'GUDANG CIMARAGAS 3', + qty: 5, + price: 45000, + purchase_amount: 225000, + transport: 0, + value_transport: 0, + total: 225000, + expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', + travel_number: '-', + }, + { + id: 4, + received_date: '2025-09-30', + po_date: '2025-09-30', + po_number: 'PO-MBU-00670', + product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG', + destination_warehouse: 'GUDANG CIMARAGAS 4', + qty: 5, + price: 45000, + purchase_amount: 225000, + transport: 0, + value_transport: 0, + total: 225000, + expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', + travel_number: '-', + }, + { + id: 5, + received_date: '2025-09-04', + po_date: '2025-09-30', + po_number: 'PO-MBU-00606', + product_name: 'DESINFEKTAN C 100 @20L', + destination_warehouse: 'GUDANG MANDALAWANGI 1', + qty: 1, + price: 800000, + purchase_amount: 800000, + transport: 0, + value_transport: 0, + total: 800000, + expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', + travel_number: '-', + }, + ], + }, + { + id: 2, + supplier: 'Supplier B', + items: [ + { + id: 6, + received_date: '2024-01-18', + po_date: '2024-01-13', + po_number: 'PO-2024-004', + product_name: 'Produk D', + destination_warehouse: 'Gudang Pusat', + qty: 200, + price: 25000, + purchase_amount: 5000000, + transport: 200000, + value_transport: 200000, + total: 5200000, + expedition_vendor_name: 'Ekspedisi GHI', + travel_number: 'SJ-004', + }, + ], + }, + { + id: 3, + supplier: 'Supplier C', + items: [ + { + id: 7, + received_date: '2024-01-20', + po_date: '2024-01-15', + po_number: 'PO-2024-006', + product_name: 'Produk F', + destination_warehouse: 'Gudang Cabang', + qty: 80, + price: 55000, + purchase_amount: 4400000, + transport: 80000, + value_transport: 80000, + total: 4480000, + expedition_vendor_name: 'Ekspedisi MNO', + travel_number: 'SJ-006', + }, + ], + }, + ], + [] + ); + + // TODO START + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tableColumns: ColumnDef[] = [ + { + header: 'No', + accessorKey: 'no', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) { + return ( +
+ {props.row.original.no} +
+ ); + } + return props.row.index + 1; + }, + }, + { + header: 'Tanggal Terima', + accessorKey: 'received_date', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) { + return ( +
+ {props.row.original.received_date} +
+ ); + } + return formatDate(props.row.original.received_date, 'DD MMM YYYY'); + }, + }, + { + header: 'Tanggal PO', + accessorKey: 'po_date', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + return formatDate(props.row.original.po_date, 'DD MMM YYYY'); + }, + }, + { + header: 'No. Referensi', + accessorKey: 'po_number', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + return props.row.original.po_number; + }, + }, + { + header: 'Nama Produk', + accessorKey: 'product_name', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + return props.row.original.product_name; + }, + }, + { + header: 'Tujuan', + accessorKey: 'destination_warehouse', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + return props.row.original.destination_warehouse; + }, + }, + { + header: 'QTY', + accessorKey: 'qty', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {value.toLocaleString()} +
+ ); + }, + }, + { + header: 'Harga Beli (Rp)', + accessorKey: 'price', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + if (isFooter) { + return ( +
+ {formatCurrency(value)} +
+ ); + } + return ( +
+ {formatCurrency(props.row.original.price)} +
+ ); + }, + }, + { + header: 'Value Harga Beli (Rp)', + accessorKey: 'purchase_amount', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + header: 'Transport (Rp)', + accessorKey: 'transport', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + if (isFooter) { + return ( +
+ {formatCurrency(value)} +
+ ); + } + return ( +
+ {formatCurrency(props.row.original.transport)} +
+ ); + }, + }, + { + header: 'Value Transport (Rp)', + accessorKey: 'value_transport', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + header: 'Jumlah (Rp)', + accessorKey: 'total', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + header: 'Ekspedisi', + accessorKey: 'expedition_vendor_name', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + return props.row.original.expedition_vendor_name; + }, + }, + { + header: 'Surat Jalan', + accessorKey: 'travel_number', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + return props.row.original.travel_number; + }, + }, + ]; + + return ( + <> + +
+ {/* TODO START */} + option.value === selectedArea) || + null + } + // @ts-expect-error TS2345 + onChange={(val) => setSelectedArea(val?.value || null)} + isLoading={isLoadingAreas} + isClearable + /> + option.value === selectedSupplier + ) || null + } + // @ts-expect-error TS2345 + onChange={(val) => setSelectedSupplier(val?.value || null)} + isLoading={isLoadingSuppliers} + isClearable + /> + option.value === selectedProduct + ) || null + } + // @ts-expect-error TS2345 + onChange={(val) => setSelectedProduct(val?.value || null)} + isLoading={isLoadingProducts} + isClearable + /> + {/* TODO END */} +
+ + {data.length === 0 ? ( +
+ Tidak ada data untuk ditampilkan. +
+ ) : ( + data.map((supplier) => { + const totalQty = supplier.items.reduce( + (sum, item) => sum + (item.qty || 0), + 0 + ); + const totalPrice = supplier.items.reduce( + (sum, item) => sum + (item.price || 0), + 0 + ); + const totalPurchaseAmount = supplier.items.reduce( + (sum, item) => sum + (item.purchase_amount || 0), + 0 + ); + const totalTransport = supplier.items.reduce( + (sum, item) => sum + (item.transport || 0), + 0 + ); + const totalValueTransport = supplier.items.reduce( + (sum, item) => sum + (item.value_transport || 0), + 0 + ); + const totalJumlah = supplier.items.reduce( + (sum, item) => sum + (item.total || 0), + 0 + ); + + const totals = { + totalQty, + totalPrice, + totalPurchaseAmount, + totalTransport, + totalValueTransport, + totalJumlah, + }; + + const footerData = + supplier.items.length > 0 + ? [ + { + id: -999, + no: 'Total', + received_date: '', + po_date: null, + po_number: null, + product_name: null, + destination_warehouse: null, + qty: totals.totalQty, + price: totals.totalPrice, + purchase_amount: totals.totalPurchaseAmount, + transport: totals.totalTransport, + value_transport: totals.totalValueTransport, + total: totals.totalJumlah, + expedition_vendor_name: null, + travel_number: null, + _isFooter: true, + }, + ] + : []; + + const totalPurchase = totals.totalJumlah; + + return ( + + 0} + className={{ + tableWrapperClassName: 'overflow-x-auto mt-4', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} + /> + + ); + }) + )} + + + ); +}; + +export default PurchasesPerSupplierTab; From 8f5dd1851a7da84ffa26c315fa00ef3741527ec1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 9 Dec 2025 18:04:33 +0700 Subject: [PATCH 03/56] refactor(FE-361): Refactor table and pagination components --- src/components/Pagination.tsx | 514 ++++++++++-------- src/components/Table.tsx | 238 ++++---- .../PurchasesPerSupplierTab.tsx | 318 ++++------- 3 files changed, 560 insertions(+), 510 deletions(-) diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index e47e480d..43b26d90 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -1,7 +1,9 @@ 'use client'; -import { ReactNode } from 'react'; +import { ChangeEventHandler, ReactNode } from 'react'; + import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; import { cn } from '@/lib/helper'; @@ -17,16 +19,18 @@ const PaginationButton = ({ disabled?: boolean; onClick?: () => void; }) => ( - + ); const EtcPaginationButton = ({ @@ -48,7 +52,7 @@ const EtcPaginationButton = ({ tabIndex={0} role='button' className={cn( - 'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square' + 'join-item btn btn-ghost p-2.5 rounded-lg! text-sm font-medium text-gray-500 aspect-square' )} > ... @@ -57,7 +61,7 @@ const EtcPaginationButton = ({
    {pages.map((pageNumber) => (
  • @@ -76,7 +80,7 @@ const EtcPaginationButton = ({ + ); + + const PrevPageButton = () => ( + + ); + + const GoToLastPageButton = () => ( + + ); + + const NextPageButton = () => ( + + ); + + const PageInfo = () => ( + + Page {currentPage} of {totalPages} + + ); return ( -
    -
    - +
    +
    +
    + +
    - {totalPages <= 7 && ( -
    - {range(1, totalPages).map((pageNumber) => ( +
    +
    + +
    + +
    + +
    + + {totalPages <= 7 && + range(1, totalPages).map((pageNumber) => ( pageChangeHandler(pageNumber)} /> ))} -
    - )} - {totalPages > 7 && ( -
    - pageChangeHandler(1)} - /> - - {totalPages >= 2 && - (currentPage <= 3 || currentPage >= totalPages - 2) && ( - pageChangeHandler(2)} - /> - )} - - {totalPages >= 2 && - currentPage > 3 && - currentPage < totalPages - 2 && ( - - )} - - {totalPages >= 3 && - (currentPage <= 4 || currentPage >= totalPages - 2) && - currentPage !== totalPages - 2 && ( - pageChangeHandler(3)} - /> - )} - - {totalPages >= 7 && - (currentPage <= 2 || currentPage >= totalPages - 2) && ( - = totalPages - 1 - ? 4 - : 1 - } - endPage={ - currentPage <= 2 || currentPage >= totalPages - 1 - ? totalPages - 3 - : currentPage === totalPages - 2 - ? totalPages - 4 - : 2 - } - onPageItemClick={pageChangeHandler} - /> - )} - - {totalPages >= 3 && - currentPage > 4 && - currentPage < totalPages - 1 && ( - pageChangeHandler(currentPage - 1)} - /> - )} - - {totalPages >= 7 && - currentPage > 3 && - currentPage < totalPages - 2 && ( - - )} - - {totalPages >= 5 && - currentPage > 2 && - currentPage < totalPages - 2 && ( - pageChangeHandler(currentPage + 1)} - /> - )} - - {totalPages >= 5 && - (currentPage <= 2 || currentPage >= totalPages - 2) && ( - pageChangeHandler(totalPages - 2)} - /> - )} - - {totalPages >= 6 && - currentPage > 2 && - currentPage < totalPages - 3 && ( - = 4 - ? currentPage + 2 - : 1 - } - endPage={ - currentPage <= 3 - ? totalPages - 2 - : currentPage >= 4 - ? totalPages - 1 - : 0 - } - onPageItemClick={pageChangeHandler} - /> - )} - - {totalPages >= 6 && - (currentPage <= 3 || currentPage >= totalPages - 3) && ( - pageChangeHandler(totalPages - 1)} - /> - )} - - {totalPages >= 7 && ( + {totalPages > 7 && ( + <> pageChangeHandler(totalPages)} + content={1} + disabled={currentPage === 1} + onClick={() => pageChangeHandler(1)} /> - )} -
    - )} - + +
    + +
    + +
    + +
    +
    + +
    + +
    -
    - +
    +
    + + + + +
    - +
    + + + +
    ); diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 5c76f44e..67920ef2 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,6 +14,7 @@ import { SortingState, OnChangeFn, Row, + HeaderContext, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -41,6 +42,7 @@ export interface TableProps { data: TData[]; columns: ColumnDef[]; pageSize?: number; + onPageSizeChange?: (pageSize: number) => void; totalItems?: number; page?: number; onPageChange?: (page: number) => void; @@ -56,8 +58,8 @@ export interface TableProps { setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); renderFooter?: boolean; - footerContent?: ReactNode; - footerData?: TData[]; + withCheckbox?: boolean; + rowOptions?: number[]; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -70,31 +72,35 @@ const emptyContentDefaultValue = (
    ); +const TABLE_DEFAULT_STYLING = { + containerClassName: 'w-full mb-20', + tableWrapperClassName: + 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', + tableClassName: 'font-inter w-full table-auto text-sm font-medium', + tableHeaderClassName: '', + headerRowClassName: '', + headerColumnClassName: 'px-4 py-3 text-base-content/50', + tableBodyClassName: '', + bodyRowClassName: 'border-t border-t-base-content/10', + bodyColumnClassName: 'px-4 py-3 text-base-content', + paginationClassName: '', + tableFooterClassName: 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', +}; + const Table = ({ data = [], columns = [], pageSize = 10, + onPageSizeChange, totalItems, page, onPageChange, isLoading = false, fuzzySearchValue, onFuzzySearchValueChange, - className = { - containerClassName: '', - tableWrapperClassName: '', - tableClassName: '', - tableHeaderClassName: '', - headerRowClassName: '', - headerColumnClassName: '', - tableBodyClassName: '', - bodyRowClassName: '', - bodyColumnClassName: '', - tableFooterClassName: '', - footerRowClassName: '', - footerColumnClassName: '', - paginationClassName: '', - }, + className = TABLE_DEFAULT_STYLING, emptyContent = emptyContentDefaultValue, sorting, setSorting, @@ -103,14 +109,19 @@ const Table = ({ setRowSelection, enableRowSelection, renderFooter = false, - footerContent, - footerData = [], + withCheckbox = false, + rowOptions = [10, 20, 50, 100], }: TableProps) => { const isServerSideTable = totalItems !== undefined && page !== undefined && onPageChange !== undefined; + const tableClassNames = { + ...TABLE_DEFAULT_STYLING, + ...className, + }; + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: pageSize, @@ -172,14 +183,6 @@ const Table = ({ const table = useReactTable(tableOptions); const { setPageSize } = table; - const footerTableOptions: TableOptions = { - columns, - data: footerData, - getCoreRowModel: getCoreRowModel(), - }; - - const footerTable = useReactTable(footerTableOptions); - const prevPageClickHandler = () => { table.previousPage(); @@ -211,68 +214,104 @@ const Table = ({ }, [pageSize, setPageSize]); return ( -
    -
    -
- +
+
+
+ {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + {headerGroup.headers.map((header) => { + const columnRelativeDepth = + header.depth - header.column.depth; + if ( + !header.isPlaceholder && + columnRelativeDepth > 1 && + header.id === header.column.id + ) { + return null; + } + let rowSpan = 1; + if (header.isPlaceholder) { + const leafs = header.getLeafHeaders(); + rowSpan = leafs[leafs.length - 1].depth - header.depth; + } + return ( + - ))} + {header.column.getCanSort() && ( +
+ + +
+ )} + + + ); + })} ))} - + {table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( - - {renderFooter && - (footerData && footerData.length > 0 - ? footerTable.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - )) - : footerContent)} + {renderFooter && ( + + {table.getAllLeafColumns().map((column) => ( + + ))} + + )}
-
- {flexRender( - header.column.columnDef.header, - header.getContext() +
+
1, + })} + > + {flexRender( + header.column.columnDef.header, + header.getContext() + )} - {header.column.getCanSort() && ( -
- - -
- )} -
-
+ {!isLoading && flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -283,24 +322,23 @@ const Table = ({ ))}
- {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} -
+ {column.columnDef.footer && + flexRender(column.columnDef.footer, { + column, + header: column.columnDef, + table, + } as HeaderContext)} +
@@ -310,7 +348,7 @@ const Table = ({ emptyContent} {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && ( -
+
({ onPrevPage={prevPageClickHandler} onNextPage={nextPageClickHandler} onPageChange={pageChangeHandler} + rowOptions={rowOptions} + onRowChange={onPageSizeChange} />
)} diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index a6f5f364..ab3d2a29 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -164,200 +164,144 @@ const PurchasesPerSupplierTab = () => { // TODO START // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tableColumns: ColumnDef[] = [ - { - header: 'No', - accessorKey: 'no', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) { - return ( -
- {props.row.original.no} -
- ); - } - return props.row.index + 1; + const getTableColumns = (totals: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tableColumns: ColumnDef[] = [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + footer: () =>
Total
, }, - }, - { - header: 'Tanggal Terima', - accessorKey: 'received_date', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) { - return ( -
- {props.row.original.received_date} -
- ); - } - return formatDate(props.row.original.received_date, 'DD MMM YYYY'); + { + id: 'received_date', + header: 'Tanggal Terima', + accessorKey: 'received_date', + cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'), }, - }, - { - header: 'Tanggal PO', - accessorKey: 'po_date', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - return formatDate(props.row.original.po_date, 'DD MMM YYYY'); + { + id: 'po_date', + header: 'Tanggal PO', + accessorKey: 'po_date', + cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'), }, - }, - { - header: 'No. Referensi', - accessorKey: 'po_number', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - return props.row.original.po_number; + { + id: 'po_number', + header: 'No. Referensi', + accessorKey: 'po_number', + cell: (props) => props.getValue() || '-', }, - }, - { - header: 'Nama Produk', - accessorKey: 'product_name', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - return props.row.original.product_name; + { + id: 'product_name', + header: 'Nama Produk', + accessorKey: 'product_name', + cell: (props) => props.getValue() || '-', }, - }, - { - header: 'Tujuan', - accessorKey: 'destination_warehouse', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - return props.row.original.destination_warehouse; + { + id: 'destination_warehouse', + header: 'Tujuan', + accessorKey: 'destination_warehouse', + cell: (props) => props.getValue() || '-', }, - }, - { - header: 'QTY', - accessorKey: 'qty', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {value.toLocaleString()} + { + id: 'qty', + header: 'QTY', + accessorKey: 'qty', + cell: (props) => { + const value = props.getValue() as number; + return
{value.toLocaleString()}
; + }, + footer: () => ( +
+ {totals.totalQty.toLocaleString()}
- ); + ), }, - }, - { - header: 'Harga Beli (Rp)', - accessorKey: 'price', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - if (isFooter) { - return ( -
- {formatCurrency(value)} -
- ); - } - return ( -
- {formatCurrency(props.row.original.price)} + { + id: 'price', + header: 'Harga Beli (Rp)', + accessorKey: 'price', + cell: (props) => { + const value = props.getValue() as number; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(totals.totalPrice)}
- ); + ), }, - }, - { - header: 'Value Harga Beli (Rp)', - accessorKey: 'purchase_amount', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} + { + id: 'purchase_amount', + header: 'Value Harga Beli (Rp)', + accessorKey: 'purchase_amount', + cell: (props) => { + const value = props.getValue() as number; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(totals.totalPurchaseAmount)}
- ); + ), }, - }, - { - header: 'Transport (Rp)', - accessorKey: 'transport', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - if (isFooter) { - return ( -
- {formatCurrency(value)} -
- ); - } - return ( -
- {formatCurrency(props.row.original.transport)} + { + id: 'transport', + header: 'Transport (Rp)', + accessorKey: 'transport', + cell: (props) => { + const value = props.getValue() as number; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(totals.totalTransport)}
- ); + ), }, - }, - { - header: 'Value Transport (Rp)', - accessorKey: 'value_transport', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} + { + id: 'value_transport', + header: 'Value Transport (Rp)', + accessorKey: 'value_transport', + cell: (props) => { + const value = props.getValue() as number; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(totals.totalValueTransport)}
- ); + ), }, - }, - { - header: 'Jumlah (Rp)', - accessorKey: 'total', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} + { + id: 'total', + header: 'Jumlah (Rp)', + accessorKey: 'total', + cell: (props) => { + const value = props.getValue() as number; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(totals.totalJumlah)}
- ); + ), }, - }, - { - header: 'Ekspedisi', - accessorKey: 'expedition_vendor_name', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - return props.row.original.expedition_vendor_name; + { + id: 'expedition_vendor_name', + header: 'Ekspedisi', + accessorKey: 'expedition_vendor_name', + cell: (props) => props.getValue() || '-', }, - }, - { - header: 'Surat Jalan', - accessorKey: 'travel_number', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - return props.row.original.travel_number; + { + id: 'travel_number', + header: 'Surat Jalan', + accessorKey: 'travel_number', + cell: (props) => props.getValue() || '-', }, - }, - ]; + ]; + return tableColumns; + }; return ( <> @@ -451,31 +395,8 @@ const PurchasesPerSupplierTab = () => { totalJumlah, }; - const footerData = - supplier.items.length > 0 - ? [ - { - id: -999, - no: 'Total', - received_date: '', - po_date: null, - po_number: null, - product_name: null, - destination_warehouse: null, - qty: totals.totalQty, - price: totals.totalPrice, - purchase_amount: totals.totalPurchaseAmount, - transport: totals.totalTransport, - value_transport: totals.totalValueTransport, - total: totals.totalJumlah, - expedition_vendor_name: null, - travel_number: null, - _isFooter: true, - }, - ] - : []; - const totalPurchase = totals.totalJumlah; + const tableColumns = getTableColumns(totals); return ( { data={supplier.items} columns={tableColumns} pageSize={10} - footerData={footerData} renderFooter={supplier.items.length > 0} className={{ tableWrapperClassName: 'overflow-x-auto mt-4', From 066c356d4f7c788f7e877538d5c381fd148513a9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 9 Dec 2025 18:44:35 +0700 Subject: [PATCH 04/56] refactor(FE-339): Export TABLE_DEFAULT_STYLING and refine table classes --- src/components/Table.tsx | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 67920ef2..9feb33e2 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -72,21 +72,22 @@ const emptyContentDefaultValue = (
); -const TABLE_DEFAULT_STYLING = { +export const TABLE_DEFAULT_STYLING = { containerClassName: 'w-full mb-20', tableWrapperClassName: 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', tableClassName: 'font-inter w-full table-auto text-sm font-medium', tableHeaderClassName: '', headerRowClassName: '', - headerColumnClassName: 'px-4 py-3 text-base-content/50', + headerColumnClassName: + 'px-4 py-3 border-base-content/10 text-base-content/50', tableBodyClassName: '', - bodyRowClassName: 'border-t border-t-base-content/10', + bodyRowClassName: 'border-t border-base-content/10', bodyColumnClassName: 'px-4 py-3 text-base-content', paginationClassName: '', - tableFooterClassName: 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + tableFooterClassName: 'font-semibold border-base-content/10', + footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', + footerColumnClassName: 'p-4 text-base-content whitespace-nowrap', }; const Table = ({ @@ -251,13 +252,15 @@ const Table = ({ { 'first:w-9 first:pr-0': withCheckbox, }, + { + 'border-b': header.colSpan > 1, + }, tableClassNames.headerColumnClassName )} >
1, + className={cn('flex items-center gap-1 min-h-full', { + 'justify-center': header.colSpan > 1, })} > {flexRender( @@ -321,13 +324,16 @@ const Table = ({ ))} - + {renderFooter && ( - + {table.getAllLeafColumns().map((column) => ( {column.columnDef.footer && flexRender(column.columnDef.footer, { From f87854ed0727addf55b5aa923f8ffc51a74b7a9a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 9 Dec 2025 18:51:36 +0700 Subject: [PATCH 05/56] refactor(FE-361): Remove wrapper top margin and clear table margin --- .../pages/report/logistic-stock/PurchasesPerSupplierTab.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index ab3d2a29..e35a5b3f 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -403,7 +403,7 @@ const PurchasesPerSupplierTab = () => { key={supplier.id} title={supplier.supplier} subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`} - className={{ wrapper: 'mt-6 w-full' }} + className={{ wrapper: 'w-full' }} collapsible={true} > { pageSize={10} renderFooter={supplier.items.length > 0} className={{ + containerClassName: 'mb-0!', tableWrapperClassName: 'overflow-x-auto mt-4', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', From 8a7149c123522f470b67db374885b8ee6a3f29bc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 9 Dec 2025 20:19:00 +0700 Subject: [PATCH 06/56] feat(FE-361): Add logistic stock tabs and tweak styles --- src/app/report/logistic-stock/page.tsx | 12 +++++++++++- .../logistic-stock/PurchasesPerSupplierTab.tsx | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/report/logistic-stock/page.tsx b/src/app/report/logistic-stock/page.tsx index 5dce741c..a0c8bd23 100644 --- a/src/app/report/logistic-stock/page.tsx +++ b/src/app/report/logistic-stock/page.tsx @@ -10,11 +10,21 @@ const LogisticStock = () => { label: 'Rekapitulasi Pembelian Per Supplier', content: , }, + { + id: '2', + label: 'Rekapitulasi Pemakaian Barang', + content: , + }, + { + id: '3', + label: 'Rekapitulasi Stock Persediaan Barang', + content: , + }, ]; return (
- +
); }; diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index e35a5b3f..b82e8e32 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -307,7 +307,7 @@ const PurchasesPerSupplierTab = () => { <>
{/* TODO START */} From a1679ba5ff5401225e5b82c5728c95d26e5e3107 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 9 Dec 2025 21:49:26 +0700 Subject: [PATCH 07/56] feat(FE-363): Add data type and date range filters --- .../PurchasesPerSupplierTab.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index b82e8e32..c930970e 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -1,6 +1,7 @@ import { useState, useMemo } from 'react'; import Card from '@/components/Card'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; @@ -12,6 +13,11 @@ const PurchasesPerSupplierTab = () => { const [selectedArea, setSelectedArea] = useState(null); const [selectedSupplier, setSelectedSupplier] = useState(null); const [selectedProduct, setSelectedProduct] = useState(null); + const [dataType, setDataType] = useState<'received_date' | 'po_date'>( + 'received_date' + ); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, @@ -26,6 +32,14 @@ const PurchasesPerSupplierTab = () => { const { options: productOptions, isLoadingOptions: isLoadingProducts } = useSelect(ProductApi.basePath, 'id', 'name', 'search'); + const dataTypeOptions = useMemo( + () => [ + { value: 'received_date', label: 'Tanggal Terima' }, + { value: 'po_date', label: 'Tanggal PO' }, + ], + [] + ); + const data = useMemo( () => [ { @@ -352,6 +366,35 @@ const PurchasesPerSupplierTab = () => { isLoading={isLoadingProducts} isClearable /> + option.value === dataType) || + null + } + // @ts-expect-error TS2345 + onChange={(val) => setDataType(val?.value || 'received_date')} + isLoading={false} + isClearable={false} + /> + + {/* TODO END */}
From e3f90a49d0125fccf1aeaf73cf07a5eda6eb5a21 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 09:22:09 +0700 Subject: [PATCH 08/56] feat(FE-361): Extract LogisticStockTabs component --- src/app/report/logistic-stock/page.tsx | 29 ++--------------- .../logistic-stock/LogisticStockTabs.tsx | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 src/components/pages/report/logistic-stock/LogisticStockTabs.tsx diff --git a/src/app/report/logistic-stock/page.tsx b/src/app/report/logistic-stock/page.tsx index a0c8bd23..77ba31ed 100644 --- a/src/app/report/logistic-stock/page.tsx +++ b/src/app/report/logistic-stock/page.tsx @@ -1,32 +1,7 @@ -'use client'; - -import Tabs from '@/components/Tabs'; -import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/PurchasesPerSupplierTab'; +import LogisticStockTabs from '@/components/pages/report/logistic-stock/LogisticStockTabs'; const LogisticStock = () => { - const tabs = [ - { - id: '1', - label: 'Rekapitulasi Pembelian Per Supplier', - content: , - }, - { - id: '2', - label: 'Rekapitulasi Pemakaian Barang', - content: , - }, - { - id: '3', - label: 'Rekapitulasi Stock Persediaan Barang', - content: , - }, - ]; - - return ( -
- -
- ); + return ; }; export default LogisticStock; diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx new file mode 100644 index 00000000..0664189f --- /dev/null +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -0,0 +1,32 @@ +'use client'; + +import Tabs from '@/components/Tabs'; +import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/PurchasesPerSupplierTab'; + +const LogisticStockTabs = () => { + const tabs = [ + { + id: '1', + label: 'Rekapitulasi Pembelian Per Supplier', + content: , + }, + { + id: '2', + label: 'Rekapitulasi Pemakaian Barang', + content: , + }, + { + id: '3', + label: 'Rekapitulasi Stock Persediaan Barang', + content: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default LogisticStockTabs; From e23b698fc7eb21183cb07305c8c16b17ddf088c2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 11:18:54 +0700 Subject: [PATCH 09/56] feat(FE-363): Add TypeScript definitions for logistic stock report --- src/types/api/report/logistic-stock.d.ts | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/types/api/report/logistic-stock.d.ts diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts new file mode 100644 index 00000000..3be8dcc2 --- /dev/null +++ b/src/types/api/report/logistic-stock.d.ts @@ -0,0 +1,30 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { Supplier } from '@/types/api/supplier/supplier'; +import { Product } from '@/types/api/product/product'; +import { Area } from '../master-data/area'; + +export type LogisticStockItems = { + id: number; + received_date: string; + po_date: string; + po_number: string; + product: Product; + area: Area; + destination_warehouse: string; + qty: number; + price: number; + transport_per_item: number; + transport_total: number; + expedition_vendor_id: number; + expedition_vendor_name: string; + travel_number: string; +}; + +export type BaseLogisticStockReport = { + id: number; + supplier: Supplier; + items: LogisticStockItems[]; +}; + +export type LogisticStockReportResponse = BaseMetadata & + BaseLogisticStockReport; From 9c7033b53a91d578318e8826d2398bfaa2537300 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 14:17:12 +0700 Subject: [PATCH 10/56] feat(FE-361): Migrate main drawer links to SidebarMenuItem --- src/config/constant.ts | 124 +++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 78 deletions(-) diff --git a/src/config/constant.ts b/src/config/constant.ts index 5b4939bc..65016462 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -1,168 +1,136 @@ -type MAIN_DRAWER_MENU = { - title: string; - link: string; - icon: string; - submenu?: MAIN_DRAWER_MENU[]; -}; +import { SidebarMenuItem } from '@/components/molecules/SidebarMenu'; -export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ +export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ { - title: 'Dashboard', + text: 'Dashboard', link: '/dashboard', - icon: 'gg:chart', + icon: 'heroicons-outline:chart-bar-square', }, - { - title: 'Produksi', + text: 'Produksi', link: '/production', - icon: 'material-symbols:conveyor-belt-outline-rounded', + icon: 'heroicons-outline:wrench-screwdriver', submenu: [ { - title: 'List Flock', + text: 'Daftar Flock', link: '/production/project-flock', - icon: 'material-symbols:list-alt-add-outline-rounded', }, - // { // DI HILANGKAN PADA VERSI REFACTORING - // title: 'Chick In', - // link: '/production/chickin', - // icon: 'mdi:home-import-outline', - // }, { - title: 'Recording', + text: 'Recording', link: '/production/recording', - icon: 'mdi:clipboard-text', }, { - title: 'Transfer ke Laying', + text: 'Transfer to Laying', link: '/production/transfer-to-laying', - icon: 'streamline:transfer-van', }, ], }, - { - title: 'Pembelian', + text: 'Pembelian', link: '/purchase', - icon: 'gg:shopping-cart', + icon: 'heroicons-outline:shopping-cart', }, - { - title: 'Penjualan', + text: 'Penjualan', link: '/marketing', - icon: 'mdi:attach-money', + icon: 'heroicons-outline:currency-dollar', }, - { - title: 'Biaya Operasional', + text: 'Biaya Operasional', link: '/expense', - icon: 'uil:wallet', + icon: 'heroicons:wallet', }, - { - title: 'Laporan', + text: 'Closing', + link: '/closing', + icon: 'heroicons-outline:presentation-chart-bar', + }, + { + text: 'Laporan', link: '/report', icon: 'mdi:chart-box-outline', submenu: [ { - title: 'Logistik & Persediaan', + text: 'Logistik & Persediaan', link: '/report/logistic-stock', - icon: 'mdi:warehouse', }, ], }, - { - title: 'Persediaan', + text: 'Persediaan', link: '/inventory', - icon: 'mdi:warehouse', + icon: 'heroicons-outline:folder', submenu: [ - // { - // title: 'Product', - // link: '/inventory/product', - // icon: 'mdi:package-variant-closed', - // }, { - title: 'Penyesuaian Stok', - link: '/inventory/adjustment', - icon: 'mdi:database-edit', + text: 'Produk', + link: '/inventory/product', }, { - title: 'Transfer Stok', + text: 'Penyesuaian Stok', + link: '/inventory/adjustment', + }, + { + text: 'Transfer Stok', link: '/inventory/movement', - icon: 'mdi:swap-horizontal', }, ], }, - { - title: 'Master Data', + text: 'Master Data', link: '/master-data', - icon: 'majesticons:data-line', + icon: 'heroicons-outline:circle-stack', submenu: [ { - title: 'Product', + text: 'Produk', link: '/master-data/product', - icon: 'fluent-mdl2:product-variant', }, { - title: 'Product Category', + text: 'Kategori Produk', link: '/master-data/product-category', - icon: 'carbon:categories', }, { - title: 'Bank', + text: 'Bank', link: '/master-data/bank', - icon: 'mdi:bank-outline', }, { - title: 'Area', + text: 'Area', link: '/master-data/area', - icon: 'majesticons:map-marker-area-line', }, { - title: 'Location', + text: 'Lokasi', link: '/master-data/location', - icon: 'mingcute:location-line', }, { - title: 'Kandang', + text: 'Kandang', link: '/master-data/kandang', - icon: 'mdi:farm-home-outline', }, { - title: 'Warehouse', + text: 'Warehouse', link: '/master-data/warehouse', - icon: 'hugeicons:warehouse', }, { - title: 'Customer', + text: 'Customer', link: '/master-data/customer', - icon: 'ix:customer', }, { - title: 'UOM', + text: 'UOM', link: '/master-data/uom', - icon: 'lsicon:measure-outline', }, { - title: 'Non-Stock', + text: 'Non-Stock', link: '/master-data/nonstock', - icon: 'fluent:box-32-regular', }, { - title: 'FCR', + text: 'FCR', link: '/master-data/fcr', - icon: 'fluent:food-chicken-leg-16-regular', }, { - title: 'Supplier', + text: 'Supplier', link: '/master-data/supplier', - icon: 'material-symbols:add-business-outline-rounded', }, { - title: 'Flock', + text: 'Flock', link: '/master-data/flock', - icon: 'material-symbols:raven-outline-rounded', }, ], }, From 5de5dcffc0b81bf28f8c44df832b69b58dc7da98 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 11 Dec 2025 15:28:26 +0700 Subject: [PATCH 11/56] fix(FE): Area import path in logistic-stock types --- src/types/api/report/logistic-stock.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts index 3be8dcc2..c8f0f391 100644 --- a/src/types/api/report/logistic-stock.d.ts +++ b/src/types/api/report/logistic-stock.d.ts @@ -1,7 +1,7 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Supplier } from '@/types/api/supplier/supplier'; import { Product } from '@/types/api/product/product'; -import { Area } from '../master-data/area'; +import { Area } from '@types/api/area/area'; export type LogisticStockItems = { id: number; From 67b5187d39a4e858bd4ccf704f3fec6f53d6439d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 11 Dec 2025 15:47:21 +0700 Subject: [PATCH 12/56] feat(FE-363): Add Logistic API service and update types --- src/services/api/logistic.ts | 40 ++++++++++++++++++++++++ src/types/api/report/logistic-stock.d.ts | 6 ++-- 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/services/api/logistic.ts diff --git a/src/services/api/logistic.ts b/src/services/api/logistic.ts new file mode 100644 index 00000000..41c25135 --- /dev/null +++ b/src/services/api/logistic.ts @@ -0,0 +1,40 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; + +export class LogisticApi extends BaseApiService< + LogisticPurchasePerSupplierReport, + unknown, + unknown +> { + constructor(basePath: string = '') { + super(basePath); + } + + async getLogisticStockReport( + area_id?: number, + supplier_id?: number, + product_id?: number, + received_date?: string, + po_date?: string, + start_date?: string, + end_date?: string + ): Promise | undefined> { + return await this.customRequest< + BaseApiResponse + >(`purchase-supplier`, { + method: 'GET', + params: { + area_id: area_id, + supplier_id: supplier_id, + product_id: product_id, + received_date: received_date, + po_date: po_date, + start_date: start_date, + end_date: end_date, + }, + }); + } +} + +export const LogisticService = new LogisticApi('/reports'); diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts index c8f0f391..27193186 100644 --- a/src/types/api/report/logistic-stock.d.ts +++ b/src/types/api/report/logistic-stock.d.ts @@ -3,7 +3,7 @@ import { Supplier } from '@/types/api/supplier/supplier'; import { Product } from '@/types/api/product/product'; import { Area } from '@types/api/area/area'; -export type LogisticStockItems = { +export type LogisticPurchasePerSupplierItems = { id: number; received_date: string; po_date: string; @@ -20,11 +20,11 @@ export type LogisticStockItems = { travel_number: string; }; -export type BaseLogisticStockReport = { +export type BaseLogisticPurchasePerSupplierReport = { id: number; supplier: Supplier; items: LogisticStockItems[]; }; -export type LogisticStockReportResponse = BaseMetadata & +export type LogisticPurchasePerSupplierReport = BaseMetadata & BaseLogisticStockReport; From bd4c51cb04722d6ea8ae9e96657ee3c420e403e3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 13 Dec 2025 10:55:15 +0700 Subject: [PATCH 13/56] refactor(FE-361,363): Refactor purchases-per-supplier report to use API --- .../PurchasesPerSupplierTab.tsx | 429 +++++++++--------- src/services/api/logistic.ts | 37 +- src/types/api/report/logistic-stock.d.ts | 11 +- 3 files changed, 267 insertions(+), 210 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index c930970e..6b641358 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -1,23 +1,69 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useCallback } from 'react'; +import { ChangeEventHandler } from 'react'; +import useSWR from 'swr'; import Card from '@/components/Card'; -import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import SelectInput, { + useSelect, + OptionType, +} from '@/components/input/SelectInput'; import DateInput from '@/components/input/DateInput'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; +import { LogisticService } from '@/services/api/logistic'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate } from '@/lib/helper'; +import { + SupplierWithItems, + LogisticPurchasePerSupplierItems, +} from '@/types/api/report/logistic-stock'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; + +interface Totals { + totalQty: number; + totalPrice: number; + totalPurchaseAmount: number; + totalTransport: number; + totalValueTransport: number; + totalJumlah: number; +} const PurchasesPerSupplierTab = () => { - const [selectedArea, setSelectedArea] = useState(null); - const [selectedSupplier, setSelectedSupplier] = useState(null); - const [selectedProduct, setSelectedProduct] = useState(null); const [dataType, setDataType] = useState<'received_date' | 'po_date'>( 'received_date' ); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); + + // ===== TABLE FILTER STATE ===== + const { + state: tableFilterState, + updateFilter, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + area_id: '', + supplier_id: '', + product_id: '', + received_date: '', + po_date: '', + start_date: '', + end_date: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, + }); + + const updateDataType = useCallback( + (newDataType: 'received_date' | 'po_date') => { + setDataType(newDataType); + updateFilter('received_date', ''); + updateFilter('po_date', ''); + }, + [updateFilter] + ); const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, @@ -40,147 +86,77 @@ const PurchasesPerSupplierTab = () => { [] ); - const data = useMemo( - () => [ - { - id: 1, - supplier: 'PT. RAJAWALI MITRA PAKANINDO', - items: [ - { - id: 1, - received_date: '2025-09-26', - po_date: '2025-09-30', - po_number: 'PO-MBU-00670', - product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG', - destination_warehouse: 'GUDANG CIMARAGAS 1', - qty: 5, - price: 45000, - purchase_amount: 225000, - transport: 0, - value_transport: 0, - total: 225000, - expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', - travel_number: '-', - }, - { - id: 2, - received_date: '2025-09-30', - po_date: '2025-09-30', - po_number: 'PO-MBU-00670', - product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG', - destination_warehouse: 'GUDANG CIMARAGAS 2', - qty: 5, - price: 45000, - purchase_amount: 225000, - transport: 0, - value_transport: 0, - total: 225000, - expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', - travel_number: '-', - }, - { - id: 3, - received_date: '2025-09-30', - po_date: '2025-09-30', - po_number: 'PO-MBU-00670', - product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG', - destination_warehouse: 'GUDANG CIMARAGAS 3', - qty: 5, - price: 45000, - purchase_amount: 225000, - transport: 0, - value_transport: 0, - total: 225000, - expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', - travel_number: '-', - }, - { - id: 4, - received_date: '2025-09-30', - po_date: '2025-09-30', - po_number: 'PO-MBU-00670', - product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG', - destination_warehouse: 'GUDANG CIMARAGAS 4', - qty: 5, - price: 45000, - purchase_amount: 225000, - transport: 0, - value_transport: 0, - total: 225000, - expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', - travel_number: '-', - }, - { - id: 5, - received_date: '2025-09-04', - po_date: '2025-09-30', - po_number: 'PO-MBU-00606', - product_name: 'DESINFEKTAN C 100 @20L', - destination_warehouse: 'GUDANG MANDALAWANGI 1', - qty: 1, - price: 800000, - purchase_amount: 800000, - transport: 0, - value_transport: 0, - total: 800000, - expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO', - travel_number: '-', - }, - ], - }, - { - id: 2, - supplier: 'Supplier B', - items: [ - { - id: 6, - received_date: '2024-01-18', - po_date: '2024-01-13', - po_number: 'PO-2024-004', - product_name: 'Produk D', - destination_warehouse: 'Gudang Pusat', - qty: 200, - price: 25000, - purchase_amount: 5000000, - transport: 200000, - value_transport: 200000, - total: 5200000, - expedition_vendor_name: 'Ekspedisi GHI', - travel_number: 'SJ-004', - }, - ], - }, - { - id: 3, - supplier: 'Supplier C', - items: [ - { - id: 7, - received_date: '2024-01-20', - po_date: '2024-01-15', - po_number: 'PO-2024-006', - product_name: 'Produk F', - destination_warehouse: 'Gudang Cabang', - qty: 80, - price: 55000, - purchase_amount: 4400000, - transport: 80000, - value_transport: 80000, - total: 4480000, - expedition_vendor_name: 'Ekspedisi MNO', - travel_number: 'SJ-006', - }, - ], - }, - ], - [] + const areaChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + updateFilter('area_id', newVal?.value ? String(newVal.value) : ''); + }, + [updateFilter] ); - // TODO START - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const getTableColumns = (totals: any) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tableColumns: ColumnDef[] = [ + const supplierChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + updateFilter('supplier_id', newVal?.value ? String(newVal.value) : ''); + }, + [updateFilter] + ); + + const productChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + updateFilter('product_id', newVal?.value ? String(newVal.value) : ''); + }, + [updateFilter] + ); + + const dataTypeChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + updateDataType( + (newVal?.value as 'received_date' | 'po_date') || 'received_date' + ); + }, + [updateDataType] + ); + + const startDateChangeHandler = useCallback< + ChangeEventHandler + >( + (e) => { + const val = e.target.value; + updateFilter('start_date', val || ''); + if (val && dataType) { + updateFilter(dataType, val); + } + }, + [updateFilter, dataType] + ); + + const endDateChangeHandler = useCallback< + ChangeEventHandler + >( + (e) => { + const val = e.target.value; + updateFilter('end_date', val || ''); + }, + [updateFilter] + ); + + // ===== DATA FETCHING ===== + const { data: response, isLoading } = useSWR( + `${LogisticService.basePath}/purchase-supplier${getTableFilterQueryString()}`, + LogisticService.getAllFetcher + ); + + const data: SupplierWithItems[] = isResponseSuccess(response) + ? (response?.data as unknown as SupplierWithItems[]) + : []; + + const getTableColumns = ( + totals: Totals + ): ColumnDef[] => { + const tableColumns: ColumnDef[] = [ { id: 'no', header: 'No', @@ -191,38 +167,53 @@ const PurchasesPerSupplierTab = () => { id: 'received_date', header: 'Tanggal Terima', accessorKey: 'received_date', - cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'), + cell: (props) => { + const value = props.row.original.received_date; + return formatDate(value, 'DD MMM YYYY'); + }, }, { id: 'po_date', header: 'Tanggal PO', accessorKey: 'po_date', - cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'), + cell: (props) => { + const value = props.row.original.po_date; + return formatDate(value, 'DD MMM YYYY'); + }, }, { id: 'po_number', header: 'No. Referensi', accessorKey: 'po_number', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const value = props.row.original.po_number; + return value || '-'; + }, }, { id: 'product_name', header: 'Nama Produk', - accessorKey: 'product_name', - cell: (props) => props.getValue() || '-', + accessorKey: 'product.name', + cell: (props) => { + const product = props.row.original.product; + return product?.name || '-'; + }, }, { id: 'destination_warehouse', header: 'Tujuan', accessorKey: 'destination_warehouse', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const value = props.row.original.destination_warehouse; + return value || '-'; + }, }, { id: 'qty', header: 'QTY', accessorKey: 'qty', cell: (props) => { - const value = props.getValue() as number; + const value = props.row.original.qty; return
{value.toLocaleString()}
; }, footer: () => ( @@ -236,7 +227,7 @@ const PurchasesPerSupplierTab = () => { header: 'Harga Beli (Rp)', accessorKey: 'price', cell: (props) => { - const value = props.getValue() as number; + const value = props.row.original.price; return
{formatCurrency(value)}
; }, footer: () => ( @@ -248,9 +239,9 @@ const PurchasesPerSupplierTab = () => { { id: 'purchase_amount', header: 'Value Harga Beli (Rp)', - accessorKey: 'purchase_amount', cell: (props) => { - const value = props.getValue() as number; + const item = props.row.original; + const value = (item.price || 0) * (item.qty || 0); return
{formatCurrency(value)}
; }, footer: () => ( @@ -262,9 +253,9 @@ const PurchasesPerSupplierTab = () => { { id: 'transport', header: 'Transport (Rp)', - accessorKey: 'transport', + accessorKey: 'transport_per_item', cell: (props) => { - const value = props.getValue() as number; + const value = props.row.original.transport_per_item; return
{formatCurrency(value)}
; }, footer: () => ( @@ -276,9 +267,9 @@ const PurchasesPerSupplierTab = () => { { id: 'value_transport', header: 'Value Transport (Rp)', - accessorKey: 'value_transport', + accessorKey: 'transport_total', cell: (props) => { - const value = props.getValue() as number; + const value = props.row.original.transport_total; return
{formatCurrency(value)}
; }, footer: () => ( @@ -290,9 +281,10 @@ const PurchasesPerSupplierTab = () => { { id: 'total', header: 'Jumlah (Rp)', - accessorKey: 'total', cell: (props) => { - const value = props.getValue() as number; + const item = props.row.original; + const value = + (item.price || 0) * (item.qty || 0) + (item.transport_total || 0); return
{formatCurrency(value)}
; }, footer: () => ( @@ -305,66 +297,88 @@ const PurchasesPerSupplierTab = () => { id: 'expedition_vendor_name', header: 'Ekspedisi', accessorKey: 'expedition_vendor_name', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const value = props.row.original.expedition_vendor_name; + return value || '-'; + }, }, { id: 'travel_number', header: 'Surat Jalan', accessorKey: 'travel_number', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const value = props.row.original.travel_number; + return value || '-'; + }, }, ]; return tableColumns; }; return ( - <> +
-
+
{/* TODO START */} option.value === selectedArea) || - null + tableFilterState.area_id + ? areaOptions.find( + (option) => + option.value === Number(tableFilterState.area_id) + ) || null + : null } - // @ts-expect-error TS2345 - onChange={(val) => setSelectedArea(val?.value || null)} + onChange={areaChangeHandler} isLoading={isLoadingAreas} isClearable + className={{ + wrapper: 'col-span-12 sm:col-span-4', + }} /> option.value === selectedSupplier - ) || null + tableFilterState.supplier_id + ? supplierOptions.find( + (option) => + option.value === Number(tableFilterState.supplier_id) + ) || null + : null } - // @ts-expect-error TS2345 - onChange={(val) => setSelectedSupplier(val?.value || null)} + onChange={supplierChangeHandler} isLoading={isLoadingSuppliers} isClearable + className={{ + wrapper: 'col-span-12 sm:col-span-4', + }} /> option.value === selectedProduct - ) || null + tableFilterState.product_id + ? productOptions.find( + (option) => + option.value === Number(tableFilterState.product_id) + ) || null + : null } - // @ts-expect-error TS2345 - onChange={(val) => setSelectedProduct(val?.value || null)} + onChange={productChangeHandler} isLoading={isLoadingProducts} isClearable + className={{ + wrapper: 'col-span-12 sm:col-span-4', + }} /> { dataTypeOptions?.find((option) => option.value === dataType) || null } - // @ts-expect-error TS2345 - onChange={(val) => setDataType(val?.value || 'received_date')} + onChange={dataTypeChangeHandler} isLoading={false} isClearable={false} + className={{ + wrapper: 'col-span-12 sm:col-span-4', + }} /> {/* TODO END */}
- {data.length === 0 ? ( + {isLoading ? ( +
Memuat data...
+ ) : data.length === 0 ? (
Tidak ada data untuk ditampilkan.
@@ -409,30 +431,30 @@ const PurchasesPerSupplierTab = () => { 0 ); const totalPrice = supplier.items.reduce( - (sum, item) => sum + (item.price || 0), - 0 - ); - const totalPurchaseAmount = supplier.items.reduce( - (sum, item) => sum + (item.purchase_amount || 0), + (sum, item) => sum + (item.price || 0) * (item.qty || 0), 0 ); const totalTransport = supplier.items.reduce( - (sum, item) => sum + (item.transport || 0), + (sum, item) => + sum + (item.transport_per_item || 0) * (item.qty || 0), 0 ); const totalValueTransport = supplier.items.reduce( - (sum, item) => sum + (item.value_transport || 0), + (sum, item) => sum + (item.transport_total || 0), 0 ); const totalJumlah = supplier.items.reduce( - (sum, item) => sum + (item.total || 0), + (sum, item) => + sum + + (item.price || 0) * (item.qty || 0) + + (item.transport_total || 0), 0 ); const totals = { totalQty, totalPrice, - totalPurchaseAmount, + totalPurchaseAmount: totalPrice, totalTransport, totalValueTransport, totalJumlah, @@ -444,7 +466,7 @@ const PurchasesPerSupplierTab = () => { return ( { pageSize={10} renderFooter={supplier.items.length > 0} className={{ - containerClassName: 'mb-0!', + containerClassName: 'w-full', tableWrapperClassName: 'overflow-x-auto mt-4', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', @@ -470,7 +492,6 @@ const PurchasesPerSupplierTab = () => { footerRowClassName: 'border-t-2 border-gray-300', footerColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - paginationClassName: 'hidden', }} /> @@ -478,7 +499,7 @@ const PurchasesPerSupplierTab = () => { }) )} - +
); }; diff --git a/src/services/api/logistic.ts b/src/services/api/logistic.ts index 41c25135..27fbb6ea 100644 --- a/src/services/api/logistic.ts +++ b/src/services/api/logistic.ts @@ -2,6 +2,12 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; +const baseLogisticApi = new BaseApiService< + LogisticPurchasePerSupplierReport, + unknown, + unknown +>('/reports'); + export class LogisticApi extends BaseApiService< LogisticPurchasePerSupplierReport, unknown, @@ -37,4 +43,33 @@ export class LogisticApi extends BaseApiService< } } -export const LogisticService = new LogisticApi('/reports'); +export const LogisticService = { + basePath: baseLogisticApi.basePath, + header: baseLogisticApi.header, + getAllFetcher: baseLogisticApi.getAllFetcher.bind(baseLogisticApi), + getSingle: baseLogisticApi.getSingle.bind(baseLogisticApi), + create: baseLogisticApi.create.bind(baseLogisticApi), + update: baseLogisticApi.update.bind(baseLogisticApi), + delete: baseLogisticApi.delete.bind(baseLogisticApi), + customRequest: baseLogisticApi.customRequest.bind(baseLogisticApi), + + // Custom method for specific endpoint + getLogisticStockReport: ( + area_id?: number, + supplier_id?: number, + product_id?: number, + received_date?: string, + po_date?: string, + start_date?: string, + end_date?: string + ) => + new LogisticApi().getLogisticStockReport( + area_id, + supplier_id, + product_id, + received_date, + po_date, + start_date, + end_date + ), +}; diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts index 27193186..a275898e 100644 --- a/src/types/api/report/logistic-stock.d.ts +++ b/src/types/api/report/logistic-stock.d.ts @@ -1,7 +1,7 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Supplier } from '@/types/api/supplier/supplier'; import { Product } from '@/types/api/product/product'; -import { Area } from '@types/api/area/area'; +import { Area } from '@/types/api/area/area'; export type LogisticPurchasePerSupplierItems = { id: number; @@ -20,11 +20,12 @@ export type LogisticPurchasePerSupplierItems = { travel_number: string; }; -export type BaseLogisticPurchasePerSupplierReport = { +export type SupplierWithItems = { id: number; supplier: Supplier; - items: LogisticStockItems[]; + items: LogisticPurchasePerSupplierItems[]; }; -export type LogisticPurchasePerSupplierReport = BaseMetadata & - BaseLogisticStockReport; +export type LogisticPurchasePerSupplierReport = BaseMetadata & { + data: SupplierWithItems[]; +}; From 0983f154d2e36f8b32b8fe47fcaa623feaa5f50e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 13 Dec 2025 10:57:56 +0700 Subject: [PATCH 14/56] refactor(FE-363): Rename SupplierWithItems type for clarity --- .../pages/report/logistic-stock/PurchasesPerSupplierTab.tsx | 6 +++--- src/types/api/report/logistic-stock.d.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index 6b641358..9e3deb95 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -15,7 +15,7 @@ import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate } from '@/lib/helper'; import { - SupplierWithItems, + LogisticPurchasePerSupplier, LogisticPurchasePerSupplierItems, } from '@/types/api/report/logistic-stock'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -149,8 +149,8 @@ const PurchasesPerSupplierTab = () => { LogisticService.getAllFetcher ); - const data: SupplierWithItems[] = isResponseSuccess(response) - ? (response?.data as unknown as SupplierWithItems[]) + const data: LogisticPurchasePerSupplier[] = isResponseSuccess(response) + ? (response?.data as unknown as LogisticPurchasePerSupplier[]) : []; const getTableColumns = ( diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts index a275898e..2f771bfa 100644 --- a/src/types/api/report/logistic-stock.d.ts +++ b/src/types/api/report/logistic-stock.d.ts @@ -20,12 +20,12 @@ export type LogisticPurchasePerSupplierItems = { travel_number: string; }; -export type SupplierWithItems = { +export type LogisticPurchasePerSupplier = { id: number; supplier: Supplier; items: LogisticPurchasePerSupplierItems[]; }; export type LogisticPurchasePerSupplierReport = BaseMetadata & { - data: SupplierWithItems[]; + data: LogisticPurchasePerSupplier[]; }; From 81f98c5f0617df6aa4c6035e8d241906f89dc5ec Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 13 Dec 2025 11:21:04 +0700 Subject: [PATCH 15/56] feat(FE-361): Add pagination control to supplier purchases table --- .../PurchasesPerSupplierTab.tsx | 77 +++++++++++-------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index 9e3deb95..b22b00a9 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -20,6 +20,7 @@ import { } from '@/types/api/report/logistic-stock'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; +import Pagination from '@/components/Pagination'; interface Totals { totalQty: number; @@ -464,37 +465,51 @@ const PurchasesPerSupplierTab = () => { const tableColumns = getTableColumns(totals); return ( - -
0} - className={{ - containerClassName: 'w-full', - tableWrapperClassName: 'overflow-x-auto mt-4', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - + <> + +
0} + className={{ + containerClassName: 'w-full', + tableWrapperClassName: 'overflow-x-auto mt-4', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: + 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + {}} + onRowChange={() => {}} + onNextPage={() => {}} + onPrevPage={() => {}} + rowOptions={[10, 25, 50, 100]} + itemsPerPage={10} + /> + + ); }) )} From 9bc632c286578861e2af457aee90b053ffc0594a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 13 Dec 2025 11:34:37 +0700 Subject: [PATCH 16/56] feat(FE-361): Add Reset button to clear report filters --- .../PurchasesPerSupplierTab.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index b22b00a9..fd6b67d5 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -21,6 +21,7 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import Pagination from '@/components/Pagination'; +import Button from '@/components/Button'; interface Totals { totalQty: number; @@ -144,6 +145,17 @@ const PurchasesPerSupplierTab = () => { [updateFilter] ); + const resetFilters = useCallback(() => { + updateFilter('area_id', ''); + updateFilter('supplier_id', ''); + updateFilter('product_id', ''); + updateFilter('received_date', ''); + updateFilter('po_date', ''); + updateFilter('start_date', ''); + updateFilter('end_date', ''); + setDataType('received_date'); + }, [updateFilter]); + // ===== DATA FETCHING ===== const { data: response, isLoading } = useSWR( `${LogisticService.basePath}/purchase-supplier${getTableFilterQueryString()}`, @@ -322,8 +334,12 @@ const PurchasesPerSupplierTab = () => { subtitle='Laporan > Rekapitulasi Pembelian Per Supplier' className={{ wrapper: 'w-full', body: 'p-1!' }} > +
+ +
- {/* TODO START */} { wrapper: 'col-span-12 sm:col-span-4', }} /> - {/* TODO END */}
{isLoading ? ( From fdb3e0481a25747038cd9628764518a03fecc024 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 13 Dec 2025 11:46:00 +0700 Subject: [PATCH 17/56] refactor(FE-363): Rename response variable to purchasePerSupplier --- .../report/logistic-stock/PurchasesPerSupplierTab.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index fd6b67d5..d74ec53f 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -157,13 +157,15 @@ const PurchasesPerSupplierTab = () => { }, [updateFilter]); // ===== DATA FETCHING ===== - const { data: response, isLoading } = useSWR( + const { data: purchasePerSupplier, isLoading } = useSWR( `${LogisticService.basePath}/purchase-supplier${getTableFilterQueryString()}`, LogisticService.getAllFetcher ); - const data: LogisticPurchasePerSupplier[] = isResponseSuccess(response) - ? (response?.data as unknown as LogisticPurchasePerSupplier[]) + const data: LogisticPurchasePerSupplier[] = isResponseSuccess( + purchasePerSupplier + ) + ? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplier[]) : []; const getTableColumns = ( From fd2e1f8b96fa0f89b6b0fa056cf51cd69f10c963 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 13 Dec 2025 11:51:41 +0700 Subject: [PATCH 18/56] refactor(FE-363): Replace LogisticService with LogisticApi instance --- .../PurchasesPerSupplierTab.tsx | 27 ++++++++---- src/services/api/logistic.ts | 41 ++----------------- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index d74ec53f..f0f9f64e 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -10,7 +10,7 @@ import DateInput from '@/components/input/DateInput'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; -import { LogisticService } from '@/services/api/logistic'; +import { LogisticApi } from '@/services/api/logistic'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate } from '@/lib/helper'; @@ -38,11 +38,7 @@ const PurchasesPerSupplierTab = () => { ); // ===== TABLE FILTER STATE ===== - const { - state: tableFilterState, - updateFilter, - toQueryString: getTableFilterQueryString, - } = useTableFilter({ + const { state: tableFilterState, updateFilter } = useTableFilter({ initial: { area_id: '', supplier_id: '', @@ -158,8 +154,23 @@ const PurchasesPerSupplierTab = () => { // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( - `${LogisticService.basePath}/purchase-supplier${getTableFilterQueryString()}`, - LogisticService.getAllFetcher + () => + LogisticApi.getLogisticStockReport( + tableFilterState.area_id ? Number(tableFilterState.area_id) : undefined, + tableFilterState.supplier_id + ? Number(tableFilterState.supplier_id) + : undefined, + tableFilterState.product_id + ? Number(tableFilterState.product_id) + : undefined, + tableFilterState.received_date || undefined, + tableFilterState.po_date || undefined, + tableFilterState.start_date || undefined, + tableFilterState.end_date || undefined + ), + { + revalidateOnFocus: false, + } ); const data: LogisticPurchasePerSupplier[] = isResponseSuccess( diff --git a/src/services/api/logistic.ts b/src/services/api/logistic.ts index 27fbb6ea..cee1c825 100644 --- a/src/services/api/logistic.ts +++ b/src/services/api/logistic.ts @@ -2,18 +2,12 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; -const baseLogisticApi = new BaseApiService< - LogisticPurchasePerSupplierReport, - unknown, - unknown ->('/reports'); - -export class LogisticApi extends BaseApiService< +export class LogisticApiService extends BaseApiService< LogisticPurchasePerSupplierReport, unknown, unknown > { - constructor(basePath: string = '') { + constructor(basePath: string) { super(basePath); } @@ -43,33 +37,4 @@ export class LogisticApi extends BaseApiService< } } -export const LogisticService = { - basePath: baseLogisticApi.basePath, - header: baseLogisticApi.header, - getAllFetcher: baseLogisticApi.getAllFetcher.bind(baseLogisticApi), - getSingle: baseLogisticApi.getSingle.bind(baseLogisticApi), - create: baseLogisticApi.create.bind(baseLogisticApi), - update: baseLogisticApi.update.bind(baseLogisticApi), - delete: baseLogisticApi.delete.bind(baseLogisticApi), - customRequest: baseLogisticApi.customRequest.bind(baseLogisticApi), - - // Custom method for specific endpoint - getLogisticStockReport: ( - area_id?: number, - supplier_id?: number, - product_id?: number, - received_date?: string, - po_date?: string, - start_date?: string, - end_date?: string - ) => - new LogisticApi().getLogisticStockReport( - area_id, - supplier_id, - product_id, - received_date, - po_date, - start_date, - end_date - ), -}; +export const LogisticApi = new LogisticApiService('/reports'); From 63c2a240d22ac06982a60929615fdcc82b6d7046 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 13 Dec 2025 12:03:56 +0700 Subject: [PATCH 19/56] refactor(FE-363): Use SWR key+fetcher with params for logistic report --- .../PurchasesPerSupplierTab.tsx | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index f0f9f64e..8a3879a5 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -154,23 +154,35 @@ const PurchasesPerSupplierTab = () => { // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( - () => - LogisticApi.getLogisticStockReport( - tableFilterState.area_id ? Number(tableFilterState.area_id) : undefined, - tableFilterState.supplier_id + () => { + const params = { + area_id: tableFilterState.area_id + ? Number(tableFilterState.area_id) + : undefined, + supplier_id: tableFilterState.supplier_id ? Number(tableFilterState.supplier_id) : undefined, - tableFilterState.product_id + product_id: tableFilterState.product_id ? Number(tableFilterState.product_id) : undefined, - tableFilterState.received_date || undefined, - tableFilterState.po_date || undefined, - tableFilterState.start_date || undefined, - tableFilterState.end_date || undefined - ), - { - revalidateOnFocus: false, - } + received_date: tableFilterState.received_date || undefined, + po_date: tableFilterState.po_date || undefined, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + }; + + return ['logistic-purchase-report', params]; + }, + ([, params]) => + LogisticApi.getLogisticStockReport( + params.area_id, + params.supplier_id, + params.product_id, + params.received_date, + params.po_date, + params.start_date, + params.end_date + ) ); const data: LogisticPurchasePerSupplier[] = isResponseSuccess( From 5c9332537c8aa638fb606149e94ae4c8f46759a8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 15 Dec 2025 11:08:05 +0700 Subject: [PATCH 20/56] feat(FE-363): Add pagination to purchases per supplier report --- .../PurchasesPerSupplierTab.tsx | 127 +++++++++++------- src/services/api/logistic.ts | 13 +- 2 files changed, 91 insertions(+), 49 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index 8a3879a5..6da12712 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -37,6 +37,10 @@ const PurchasesPerSupplierTab = () => { 'received_date' ); + // ===== PAGINATION STATE ===== + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + // ===== TABLE FILTER STATE ===== const { state: tableFilterState, updateFilter } = useTableFilter({ initial: { @@ -169,6 +173,8 @@ const PurchasesPerSupplierTab = () => { po_date: tableFilterState.po_date || undefined, start_date: tableFilterState.start_date || undefined, end_date: tableFilterState.end_date || undefined, + page: currentPage, + limit: pageSize, }; return ['logistic-purchase-report', params]; @@ -181,7 +187,9 @@ const PurchasesPerSupplierTab = () => { params.received_date, params.po_date, params.start_date, - params.end_date + params.end_date, + params.page, + params.limit ) ); @@ -191,6 +199,32 @@ const PurchasesPerSupplierTab = () => { ? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplier[]) : []; + const meta = + isResponseSuccess(purchasePerSupplier) && 'meta' in purchasePerSupplier + ? purchasePerSupplier.meta + : undefined; + + // ===== PAGINATION HANDLERS ===== + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const handleRowChange = (pageSize: number) => { + setPageSize(pageSize); + }; + + const handleNextPage = () => { + if (meta && currentPage < meta.total_pages) { + setCurrentPage(currentPage + 1); + } + }; + + const handlePrevPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + const getTableColumns = ( totals: Totals ): ColumnDef[] => { @@ -505,55 +539,56 @@ const PurchasesPerSupplierTab = () => { const tableColumns = getTableColumns(totals); return ( - <> - -
0} - className={{ - containerClassName: 'w-full', - tableWrapperClassName: 'overflow-x-auto mt-4', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: - 'border-b border-b-gray-200 bg-gray-50', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - {}} - onRowChange={() => {}} - onNextPage={() => {}} - onPrevPage={() => {}} - rowOptions={[10, 25, 50, 100]} - itemsPerPage={10} - /> - - + +
0} + className={{ + containerClassName: 'w-full', + tableWrapperClassName: 'overflow-x-auto mt-4', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} + /> + ); }) )} + {meta && data.length > 0 && ( +
+ +
+ )} ); }; diff --git a/src/services/api/logistic.ts b/src/services/api/logistic.ts index cee1c825..611c601d 100644 --- a/src/services/api/logistic.ts +++ b/src/services/api/logistic.ts @@ -18,11 +18,13 @@ export class LogisticApiService extends BaseApiService< received_date?: string, po_date?: string, start_date?: string, - end_date?: string + end_date?: string, + page?: number, + limit?: number ): Promise | undefined> { return await this.customRequest< BaseApiResponse - >(`purchase-supplier`, { + >(`logistic-stock`, { method: 'GET', params: { area_id: area_id, @@ -32,9 +34,14 @@ export class LogisticApiService extends BaseApiService< po_date: po_date, start_date: start_date, end_date: end_date, + page: page, + limit: limit, }, }); } } -export const LogisticApi = new LogisticApiService('/reports'); +// TODO: REPLACE WITH PRODUCTION URL +export const LogisticApi = new LogisticApiService( + 'http://localhost:4010/api/report' +); From 69eaae6d43cf9fd08297dbd75c8b41aa72048566 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 15 Dec 2025 11:41:12 +0700 Subject: [PATCH 21/56] feat(FE=361,363): Add Submit button to PurchasesPerSupplierTab --- .../PurchasesPerSupplierTab.tsx | 82 ++++++++++++------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index 6da12712..5b2e450d 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -41,6 +41,9 @@ const PurchasesPerSupplierTab = () => { const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + // ===== TABLE FILTER STATE ===== const { state: tableFilterState, updateFilter } = useTableFilter({ initial: { @@ -63,6 +66,7 @@ const PurchasesPerSupplierTab = () => { setDataType(newDataType); updateFilter('received_date', ''); updateFilter('po_date', ''); + setIsSubmitted(false); }, [updateFilter] ); @@ -92,6 +96,7 @@ const PurchasesPerSupplierTab = () => { (val: OptionType | OptionType[] | null) => { const newVal = val as OptionType; updateFilter('area_id', newVal?.value ? String(newVal.value) : ''); + setIsSubmitted(false); }, [updateFilter] ); @@ -100,6 +105,7 @@ const PurchasesPerSupplierTab = () => { (val: OptionType | OptionType[] | null) => { const newVal = val as OptionType; updateFilter('supplier_id', newVal?.value ? String(newVal.value) : ''); + setIsSubmitted(false); }, [updateFilter] ); @@ -108,6 +114,7 @@ const PurchasesPerSupplierTab = () => { (val: OptionType | OptionType[] | null) => { const newVal = val as OptionType; updateFilter('product_id', newVal?.value ? String(newVal.value) : ''); + setIsSubmitted(false); }, [updateFilter] ); @@ -118,6 +125,7 @@ const PurchasesPerSupplierTab = () => { updateDataType( (newVal?.value as 'received_date' | 'po_date') || 'received_date' ); + setIsSubmitted(false); }, [updateDataType] ); @@ -128,11 +136,9 @@ const PurchasesPerSupplierTab = () => { (e) => { const val = e.target.value; updateFilter('start_date', val || ''); - if (val && dataType) { - updateFilter(dataType, val); - } + setIsSubmitted(false); }, - [updateFilter, dataType] + [updateFilter] ); const endDateChangeHandler = useCallback< @@ -141,6 +147,7 @@ const PurchasesPerSupplierTab = () => { (e) => { const val = e.target.value; updateFilter('end_date', val || ''); + setIsSubmitted(false); }, [updateFilter] ); @@ -154,31 +161,39 @@ const PurchasesPerSupplierTab = () => { updateFilter('start_date', ''); updateFilter('end_date', ''); setDataType('received_date'); + setIsSubmitted(false); }, [updateFilter]); + const handleSubmit = useCallback(() => { + setIsSubmitted(true); + setCurrentPage(1); + }, []); + // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( - () => { - const params = { - area_id: tableFilterState.area_id - ? Number(tableFilterState.area_id) - : undefined, - supplier_id: tableFilterState.supplier_id - ? Number(tableFilterState.supplier_id) - : undefined, - product_id: tableFilterState.product_id - ? Number(tableFilterState.product_id) - : undefined, - received_date: tableFilterState.received_date || undefined, - po_date: tableFilterState.po_date || undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - page: currentPage, - limit: pageSize, - }; + isSubmitted + ? () => { + const params = { + area_id: tableFilterState.area_id + ? Number(tableFilterState.area_id) + : undefined, + supplier_id: tableFilterState.supplier_id + ? Number(tableFilterState.supplier_id) + : undefined, + product_id: tableFilterState.product_id + ? Number(tableFilterState.product_id) + : undefined, + received_date: tableFilterState.received_date || undefined, + po_date: tableFilterState.po_date || undefined, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + page: currentPage, + limit: pageSize, + }; - return ['logistic-purchase-report', params]; - }, + return ['logistic-purchase-report', params]; + } + : null, ([, params]) => LogisticApi.getLogisticStockReport( params.area_id, @@ -393,10 +408,13 @@ const PurchasesPerSupplierTab = () => { subtitle='Laporan > Rekapitulasi Pembelian Per Supplier' className={{ wrapper: 'w-full', body: 'p-1!' }} > -
- +
{ />
- {isLoading ? ( -
Memuat data...
+ {!isSubmitted ? ( +
+ Silakan pilih filter dan klik tombol Submit untuk menampilkan data. +
+ ) : isLoading ? ( +
+ +
) : data.length === 0 ? (
- Tidak ada data untuk ditampilkan. + Tidak ada data yang dapat ditampilkan...
) : ( data.map((supplier) => { From 3f78cfdb632f8f49d2782c20fb4491d9f815441f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 15 Dec 2025 14:04:16 +0700 Subject: [PATCH 22/56] refactor(FE): refactor Dropdown component API and Navbar usage --- src/components/Dropdown.tsx | 114 ++++++++++++++++++++++++++ src/components/Navbar.tsx | 11 ++- src/components/dropdown/Dropdown.tsx | 116 --------------------------- 3 files changed, 121 insertions(+), 120 deletions(-) create mode 100644 src/components/Dropdown.tsx delete mode 100644 src/components/dropdown/Dropdown.tsx diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx new file mode 100644 index 00000000..5bfa7a7d --- /dev/null +++ b/src/components/Dropdown.tsx @@ -0,0 +1,114 @@ +import React, { ReactNode, useState, useRef } from 'react'; + +import { cn } from '@/lib/helper'; + +export interface DropdownProps { + trigger: ReactNode; + children: ReactNode; + className?: { + wrapper?: string; + trigger?: string; + content?: string; + }; + align?: 'start' | 'center' | 'end'; + direction?: 'top' | 'bottom' | 'left' | 'right'; + hover?: boolean; + defaultOpen?: boolean; + open?: boolean; + close?: boolean; + controlled?: boolean; +} + +const Dropdown = ({ + trigger, + children, + className, + align, + direction, + hover, + defaultOpen = false, + open, + close, + controlled = false, +}: DropdownProps) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + const dropdownRef = useRef(null); + + const toggleDropdown = () => { + if (!controlled) { + const newState = !isOpen; + setIsOpen(newState); + } + }; + + const getWrapperClasses = () => { + const openState = controlled ? open : isOpen; + + return cn( + 'dropdown', + { + 'dropdown-start': align === 'start', + 'dropdown-center': align === 'center', + 'dropdown-end': align === 'end', + 'dropdown-top': direction === 'top', + 'dropdown-bottom': direction === 'bottom', + 'dropdown-left': direction === 'left', + 'dropdown-right': direction === 'right', + 'dropdown-hover': hover, + 'dropdown-open': openState && !close, + 'dropdown-close': close, + }, + className?.wrapper + ); + }; + + const getTriggerClasses = () => { + return cn(className?.trigger); + }; + + const getContentClasses = () => { + return cn( + 'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box', + className?.content + ); + }; + + if (controlled) { + return ( +
+ {trigger} + {open && !close && ( +
+ {children} +
+ )} +
+ ); + } + + return ( +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleDropdown(); + } + }} + > + {trigger} +
+ {!close && ( +
+ {children} +
+ )} +
+ ); +}; + +export default Dropdown; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index bee92a57..280217a0 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -7,7 +7,7 @@ import { Icon } from '@iconify/react'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Button from '@/components/Button'; -import Dropdown from '@/components/dropdown/Dropdown'; +import Dropdown from '@/components/Dropdown'; import { useAuth } from '@/services/hooks/useAuth'; import { AuthApi } from '@/services/api/auth'; @@ -54,7 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
@@ -62,9 +63,11 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
} - contentClassName='w-52 mt-3' + className={{ + content: 'w-52 mt-3', + }} > - + diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx deleted file mode 100644 index 4489231d..00000000 --- a/src/components/dropdown/Dropdown.tsx +++ /dev/null @@ -1,116 +0,0 @@ -'use client'; - -import { ReactNode, useRef, useEffect, useState } from 'react'; -import { cn } from '@/lib/helper'; - -interface DropdownProps { - trigger: ReactNode; - children: ReactNode; - position?: - | 'top' - | 'bottom' - | 'left' - | 'right' - | 'top-start' - | 'top-end' - | 'bottom-start' - | 'bottom-end' - | 'left-start' - | 'left-end' - | 'right-start' - | 'right-end'; - align?: 'start' | 'center' | 'end'; - hover?: boolean; - className?: string; - contentClassName?: string; -} - -const Dropdown = ({ - trigger, - children, - position = 'bottom', - align = 'start', - hover = false, - className, - contentClassName, -}: DropdownProps) => { - const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); - - // Handle click outside to close dropdown - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setIsOpen(false); - } - }; - - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen]); - - // Build position classes - const getPositionClasses = () => { - const classes: string[] = []; - - // Handle combined positions like 'top-start' - if (position.includes('-')) { - const [pos, al] = position.split('-'); - classes.push(`dropdown-${pos}`); - classes.push(`dropdown-${al}`); - } else { - classes.push(`dropdown-${position}`); - if (align !== 'start') { - classes.push(`dropdown-${align}`); - } - } - - return classes.join(' '); - }; - - const handleToggle = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - // alert('clicked'); - setIsOpen(!isOpen); - }; - - return ( -
- {/* Trigger Button */} -
- {trigger} -
- - {/* Dropdown Content - Only render when open */} - {isOpen && ( -
setIsOpen(false)} // Close on item click - > - {children} -
- )} -
- ); -}; - -export default Dropdown; From 3c3c2345c78579fe9e488c009427eec3b18f9e19 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 15 Dec 2025 14:14:34 +0700 Subject: [PATCH 23/56] feat(FE-361): Update action buttons in PurchasesPerSupplierTab --- .../report/logistic-stock/PurchasesPerSupplierTab.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx index 5b2e450d..12ce83c2 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx @@ -408,13 +408,14 @@ const PurchasesPerSupplierTab = () => { subtitle='Laporan > Rekapitulasi Pembelian Per Supplier' className={{ wrapper: 'w-full', body: 'p-1!' }} > -
+
+ - +
Date: Mon, 15 Dec 2025 15:10:22 +0700 Subject: [PATCH 24/56] feat(FE-361,363): Add export dropdown to PurchasesPerSupplier tab --- .../logistic-stock/LogisticStockTabs.tsx | 6 +++--- .../export/PurchasesPerSupplierExport.tsx | 0 .../{ => tab}/PurchasesPerSupplierTab.tsx | 21 ++++++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx rename src/components/pages/report/logistic-stock/{ => tab}/PurchasesPerSupplierTab.tsx (96%) diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index 0664189f..a3a3b062 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -1,7 +1,7 @@ 'use client'; import Tabs from '@/components/Tabs'; -import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/PurchasesPerSupplierTab'; +import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; const LogisticStockTabs = () => { const tabs = [ @@ -13,12 +13,12 @@ const LogisticStockTabs = () => { { id: '2', label: 'Rekapitulasi Pemakaian Barang', - content: , + content: 'Rekapitulasi Pemakaian Barang Tab', }, { id: '3', label: 'Rekapitulasi Stock Persediaan Barang', - content: , + content: 'Rekapitulasi Stock Persediaan Barang Tab', }, ]; diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx similarity index 96% rename from src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx rename to src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 12ce83c2..18d729aa 100644 --- a/src/components/pages/report/logistic-stock/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -22,6 +22,9 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import Pagination from '@/components/Pagination'; import Button from '@/components/Button'; +import Dropdown from '@/components/Dropdown'; +import MenuItem from '@/components/menu/MenuItem'; +import Menu from '@/components/menu/Menu'; interface Totals { totalQty: number; @@ -219,6 +222,14 @@ const PurchasesPerSupplierTab = () => { ? purchasePerSupplier.meta : undefined; + const handleExportExcel = useCallback(() => { + alert('Export to Excel functionality to be implemented.'); + }, []); + + const handleExportPdf = useCallback(() => { + alert('Export to PDF functionality to be implemented.'); + }, []); + // ===== PAGINATION HANDLERS ===== const handlePageChange = (page: number) => { setCurrentPage(page); @@ -415,7 +426,15 @@ const PurchasesPerSupplierTab = () => { - + Export} + align='end' + > + + + + +
Date: Mon, 15 Dec 2025 16:29:27 +0700 Subject: [PATCH 25/56] fix(FE-vulnerability): Bump Next.js to 15.5.9 --- package-lock.json | 28 +++++++++++++++++++--------- package.json | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0212474..c0bf87aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.7", + "next": "15.5.9", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1082,9 +1082,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1855,6 +1855,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1924,6 +1925,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2447,6 +2449,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3060,7 +3063,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.5.8", @@ -3516,6 +3520,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3689,6 +3694,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,12 +5660,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -6167,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6197,6 +6204,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7083,6 +7091,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7250,6 +7259,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 52fc6ce2..f5bd2d0f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.7", + "next": "15.5.9", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", From 7ea9e10ad20d992d0ce207f4cb50b2b6f5234b04 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 15 Dec 2025 16:34:00 +0700 Subject: [PATCH 26/56] chore(FE): Add xlsx dependency from SheetJS CDN --- package-lock.json | 13 +++++++++++++ package.json | 1 + 2 files changed, 14 insertions(+) diff --git a/package-lock.json b/package-lock.json index c0bf87aa..0c0c75ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "swr": "^2.3.6", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.6", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yup": "^1.7.0", "zustand": "^5.0.8" }, @@ -7535,6 +7536,18 @@ "node": ">=0.10.0" } }, + "node_modules/xlsx": { + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", + "license": "Apache-2.0", + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/package.json b/package.json index f5bd2d0f..d0b99b80 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "swr": "^2.3.6", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.6", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yup": "^1.7.0", "zustand": "^5.0.8" }, From 4d997256ad0014d1ab1016c34b161a6199428023 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 10:00:39 +0700 Subject: [PATCH 27/56] refactor(FE-363): Update logistic report API endpoint and types --- src/services/api/logistic.ts | 4 +-- src/types/api/report/logistic-stock.d.ts | 42 ++++++++++-------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/services/api/logistic.ts b/src/services/api/logistic.ts index 611c601d..94b7a2cf 100644 --- a/src/services/api/logistic.ts +++ b/src/services/api/logistic.ts @@ -24,7 +24,7 @@ export class LogisticApiService extends BaseApiService< ): Promise | undefined> { return await this.customRequest< BaseApiResponse - >(`logistic-stock`, { + >(`purchase-supplier`, { method: 'GET', params: { area_id: area_id, @@ -43,5 +43,5 @@ export class LogisticApiService extends BaseApiService< // TODO: REPLACE WITH PRODUCTION URL export const LogisticApi = new LogisticApiService( - 'http://localhost:4010/api/report' + 'http://localhost:4010/api/reports/logistics' ); diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts index 2f771bfa..77d0b5d3 100644 --- a/src/types/api/report/logistic-stock.d.ts +++ b/src/types/api/report/logistic-stock.d.ts @@ -1,31 +1,23 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Supplier } from '@/types/api/supplier/supplier'; import { Product } from '@/types/api/product/product'; -import { Area } from '@/types/api/area/area'; - -export type LogisticPurchasePerSupplierItems = { - id: number; - received_date: string; - po_date: string; - po_number: string; - product: Product; - area: Area; - destination_warehouse: string; - qty: number; - price: number; - transport_per_item: number; - transport_total: number; - expedition_vendor_id: number; - expedition_vendor_name: string; - travel_number: string; -}; - -export type LogisticPurchasePerSupplier = { - id: number; - supplier: Supplier; - items: LogisticPurchasePerSupplierItems[]; -}; +import { Warehouse } from '@/types/api/warehouse/warehouse'; export type LogisticPurchasePerSupplierReport = BaseMetadata & { - data: LogisticPurchasePerSupplier[]; + rows: { + supplier: Supplier; + receive_date: string; + po_date: string; + po_number: string; + product: Product; + warehouse: Warehouse; + qty: number; + unit_price: number; + purchase_value: number; + transport_unit_price: number; + transport_value: number; + total_amount: number; + expedition: string; + delivery_number: string; + }[]; }; From 31b2a5a5487179e3d1a55a8b821276fa922b9103 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 10:01:13 +0700 Subject: [PATCH 28/56] refactor(FE-361,363): Adapt PurchasesPerSupplier to new report shape --- .../tab/PurchasesPerSupplierTab.tsx | 110 +++++++++++------- 1 file changed, 69 insertions(+), 41 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 18d729aa..e1a55fa9 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -14,10 +14,7 @@ import { LogisticApi } from '@/services/api/logistic'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate } from '@/lib/helper'; -import { - LogisticPurchasePerSupplier, - LogisticPurchasePerSupplierItems, -} from '@/types/api/report/logistic-stock'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import Pagination from '@/components/Pagination'; @@ -211,11 +208,14 @@ const PurchasesPerSupplierTab = () => { ) ); - const data: LogisticPurchasePerSupplier[] = isResponseSuccess( - purchasePerSupplier - ) - ? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplier[]) - : []; + const data: LogisticPurchasePerSupplierReport['rows'] = useMemo( + () => + isResponseSuccess(purchasePerSupplier) + ? (purchasePerSupplier?.data + ?.rows as LogisticPurchasePerSupplierReport['rows']) || [] + : [], + [purchasePerSupplier] + ); const meta = isResponseSuccess(purchasePerSupplier) && 'meta' in purchasePerSupplier @@ -245,6 +245,36 @@ const PurchasesPerSupplierTab = () => { } }; + interface GroupedSupplierData { + id: number; + supplier: { + id: number; + name: string; + }; + items: LogisticPurchasePerSupplierReport['rows']; + } + + // Group data by supplier for display + const groupedData = useMemo(() => { + const groups: { [key: number]: GroupedSupplierData } = {}; + + data.forEach((item) => { + const supplierId = item.supplier?.id; + if (supplierId && !groups[supplierId]) { + groups[supplierId] = { + id: supplierId, + supplier: item.supplier, + items: [], + }; + } + if (groups[supplierId]) { + groups[supplierId].items.push(item); + } + }); + + return Object.values(groups); + }, [data]); + const handlePrevPage = () => { if (currentPage > 1) { setCurrentPage(currentPage - 1); @@ -253,20 +283,23 @@ const PurchasesPerSupplierTab = () => { const getTableColumns = ( totals: Totals - ): ColumnDef[] => { - const tableColumns: ColumnDef[] = [ + ): ColumnDef[] => { + const tableColumns: ColumnDef< + LogisticPurchasePerSupplierReport['rows'][0] + >[] = [ { id: 'no', header: 'No', cell: (props) => props.row.index + 1, footer: () =>
Total
, }, + { id: 'received_date', header: 'Tanggal Terima', - accessorKey: 'received_date', + accessorKey: 'receive_date', cell: (props) => { - const value = props.row.original.received_date; + const value = props.row.original.receive_date; return formatDate(value, 'DD MMM YYYY'); }, }, @@ -300,10 +333,10 @@ const PurchasesPerSupplierTab = () => { { id: 'destination_warehouse', header: 'Tujuan', - accessorKey: 'destination_warehouse', + accessorKey: 'warehouse.name', cell: (props) => { - const value = props.row.original.destination_warehouse; - return value || '-'; + const warehouse = props.row.original.warehouse; + return warehouse?.name || '-'; }, }, { @@ -323,9 +356,9 @@ const PurchasesPerSupplierTab = () => { { id: 'price', header: 'Harga Beli (Rp)', - accessorKey: 'price', + accessorKey: 'unit_price', cell: (props) => { - const value = props.row.original.price; + const value = props.row.original.unit_price; return
{formatCurrency(value)}
; }, footer: () => ( @@ -337,9 +370,9 @@ const PurchasesPerSupplierTab = () => { { id: 'purchase_amount', header: 'Value Harga Beli (Rp)', + accessorKey: 'purchase_value', cell: (props) => { - const item = props.row.original; - const value = (item.price || 0) * (item.qty || 0); + const value = props.row.original.purchase_value; return
{formatCurrency(value)}
; }, footer: () => ( @@ -351,9 +384,9 @@ const PurchasesPerSupplierTab = () => { { id: 'transport', header: 'Transport (Rp)', - accessorKey: 'transport_per_item', + accessorKey: 'transport_unit_price', cell: (props) => { - const value = props.row.original.transport_per_item; + const value = props.row.original.transport_unit_price; return
{formatCurrency(value)}
; }, footer: () => ( @@ -365,9 +398,9 @@ const PurchasesPerSupplierTab = () => { { id: 'value_transport', header: 'Value Transport (Rp)', - accessorKey: 'transport_total', + accessorKey: 'transport_value', cell: (props) => { - const value = props.row.original.transport_total; + const value = props.row.original.transport_value; return
{formatCurrency(value)}
; }, footer: () => ( @@ -379,10 +412,9 @@ const PurchasesPerSupplierTab = () => { { id: 'total', header: 'Jumlah (Rp)', + accessorKey: 'total_amount', cell: (props) => { - const item = props.row.original; - const value = - (item.price || 0) * (item.qty || 0) + (item.transport_total || 0); + const value = props.row.original.total_amount; return
{formatCurrency(value)}
; }, footer: () => ( @@ -394,18 +426,18 @@ const PurchasesPerSupplierTab = () => { { id: 'expedition_vendor_name', header: 'Ekspedisi', - accessorKey: 'expedition_vendor_name', + accessorKey: 'expedition', cell: (props) => { - const value = props.row.original.expedition_vendor_name; + const value = props.row.original.expedition; return value || '-'; }, }, { id: 'travel_number', header: 'Surat Jalan', - accessorKey: 'travel_number', + accessorKey: 'delivery_number', cell: (props) => { - const value = props.row.original.travel_number; + const value = props.row.original.delivery_number; return value || '-'; }, }, @@ -544,29 +576,25 @@ const PurchasesPerSupplierTab = () => { Tidak ada data yang dapat ditampilkan...
) : ( - data.map((supplier) => { + groupedData.map((supplier) => { const totalQty = supplier.items.reduce( (sum, item) => sum + (item.qty || 0), 0 ); const totalPrice = supplier.items.reduce( - (sum, item) => sum + (item.price || 0) * (item.qty || 0), + (sum, item) => sum + (item.purchase_value || 0), 0 ); const totalTransport = supplier.items.reduce( - (sum, item) => - sum + (item.transport_per_item || 0) * (item.qty || 0), + (sum, item) => sum + (item.transport_value || 0), 0 ); const totalValueTransport = supplier.items.reduce( - (sum, item) => sum + (item.transport_total || 0), + (sum, item) => sum + (item.transport_value || 0), 0 ); const totalJumlah = supplier.items.reduce( - (sum, item) => - sum + - (item.price || 0) * (item.qty || 0) + - (item.transport_total || 0), + (sum, item) => sum + (item.total_amount || 0), 0 ); @@ -587,7 +615,7 @@ const PurchasesPerSupplierTab = () => { key={supplier.id} title={supplier.supplier.name} subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`} - className={{ wrapper: 'w-full' }} + className={{ wrapper: 'w-full', body: 'py-6! p-0' }} collapsible={true} >
Date: Tue, 16 Dec 2025 10:26:31 +0700 Subject: [PATCH 29/56] feat(FE-361,363): Add product category filter and API params --- .../tab/PurchasesPerSupplierTab.tsx | 146 +++++++++++------- src/services/api/logistic.ts | 6 + 2 files changed, 93 insertions(+), 59 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index e1a55fa9..4144ae94 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -10,6 +10,7 @@ import DateInput from '@/components/input/DateInput'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; +import { ProductCategoryApi } from '@/services/api/master-data'; import { LogisticApi } from '@/services/api/logistic'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; @@ -33,10 +34,6 @@ interface Totals { } const PurchasesPerSupplierTab = () => { - const [dataType, setDataType] = useState<'received_date' | 'po_date'>( - 'received_date' - ); - // ===== PAGINATION STATE ===== const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); @@ -50,10 +47,13 @@ const PurchasesPerSupplierTab = () => { area_id: '', supplier_id: '', product_id: '', + product_category_id: '', received_date: '', po_date: '', start_date: '', end_date: '', + sort_by: '', + filter_by: 'received_date', }, paramMap: { page: 'page', @@ -61,16 +61,6 @@ const PurchasesPerSupplierTab = () => { }, }); - const updateDataType = useCallback( - (newDataType: 'received_date' | 'po_date') => { - setDataType(newDataType); - updateFilter('received_date', ''); - updateFilter('po_date', ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, 'id', @@ -84,6 +74,11 @@ const PurchasesPerSupplierTab = () => { const { options: productOptions, isLoadingOptions: isLoadingProducts } = useSelect(ProductApi.basePath, 'id', 'name', 'search'); + const { + options: productCategoryOptions, + isLoadingOptions: isLoadingProductCategories, + } = useSelect(ProductCategoryApi.basePath, 'id', 'name', 'search'); + const dataTypeOptions = useMemo( () => [ { value: 'received_date', label: 'Tanggal Terima' }, @@ -119,15 +114,29 @@ const PurchasesPerSupplierTab = () => { [updateFilter] ); - const dataTypeChangeHandler = useCallback( + const productCategoryChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const newVal = val as OptionType; - updateDataType( - (newVal?.value as 'received_date' | 'po_date') || 'received_date' + updateFilter( + 'product_category_id', + newVal?.value ? String(newVal.value) : '' ); setIsSubmitted(false); }, - [updateDataType] + [updateFilter] + ); + + const dataTypeChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + const filterValue = + (newVal?.value as 'received_date' | 'po_date') || 'received_date'; + updateFilter('filter_by', filterValue); + updateFilter('received_date', ''); + updateFilter('po_date', ''); + setIsSubmitted(false); + }, + [updateFilter] ); const startDateChangeHandler = useCallback< @@ -156,11 +165,13 @@ const PurchasesPerSupplierTab = () => { updateFilter('area_id', ''); updateFilter('supplier_id', ''); updateFilter('product_id', ''); + updateFilter('product_category_id', ''); updateFilter('received_date', ''); updateFilter('po_date', ''); updateFilter('start_date', ''); updateFilter('end_date', ''); - setDataType('received_date'); + updateFilter('sort_by', ''); + updateFilter('filter_by', 'received_date'); setIsSubmitted(false); }, [updateFilter]); @@ -183,10 +194,21 @@ const PurchasesPerSupplierTab = () => { product_id: tableFilterState.product_id ? Number(tableFilterState.product_id) : undefined, - received_date: tableFilterState.received_date || undefined, - po_date: tableFilterState.po_date || undefined, + product_category_id: tableFilterState.product_category_id + ? Number(tableFilterState.product_category_id) + : undefined, + received_date: + tableFilterState.filter_by === 'received_date' + ? tableFilterState.start_date || undefined + : undefined, + po_date: + tableFilterState.filter_by === 'po_date' + ? tableFilterState.start_date || undefined + : undefined, start_date: tableFilterState.start_date || undefined, end_date: tableFilterState.end_date || undefined, + sort_by: tableFilterState.sort_by || undefined, + filter_by: tableFilterState.filter_by || undefined, page: currentPage, limit: pageSize, }; @@ -199,10 +221,13 @@ const PurchasesPerSupplierTab = () => { params.area_id, params.supplier_id, params.product_id, + params.product_category_id, params.received_date, params.po_date, params.start_date, params.end_date, + params.sort_by, + params.filter_by, params.page, params.limit ) @@ -254,7 +279,6 @@ const PurchasesPerSupplierTab = () => { items: LogisticPurchasePerSupplierReport['rows']; } - // Group data by supplier for display const groupedData = useMemo(() => { const groups: { [key: number]: GroupedSupplierData } = {}; @@ -468,7 +492,7 @@ const PurchasesPerSupplierTab = () => { -
+
{ onChange={areaChangeHandler} isLoading={isLoadingAreas} isClearable - className={{ - wrapper: 'col-span-12 sm:col-span-4', - }} /> { onChange={supplierChangeHandler} isLoading={isLoadingSuppliers} isClearable - className={{ - wrapper: 'col-span-12 sm:col-span-4', - }} /> { onChange={productChangeHandler} isLoading={isLoadingProducts} isClearable - className={{ - wrapper: 'col-span-12 sm:col-span-4', - }} + /> +
+
+ + option.value === + Number(tableFilterState.product_category_id) + ) || null + : null + } + onChange={productCategoryChangeHandler} + isLoading={isLoadingProductCategories} + isClearable /> option.value === dataType) || - null + dataTypeOptions?.find( + (option) => option.value === tableFilterState.filter_by + ) || null } onChange={dataTypeChangeHandler} isLoading={false} isClearable={false} - className={{ - wrapper: 'col-span-12 sm:col-span-4', - }} - /> - - +
+ + +
{!isSubmitted ? ( diff --git a/src/services/api/logistic.ts b/src/services/api/logistic.ts index 94b7a2cf..64930e19 100644 --- a/src/services/api/logistic.ts +++ b/src/services/api/logistic.ts @@ -15,10 +15,13 @@ export class LogisticApiService extends BaseApiService< area_id?: number, supplier_id?: number, product_id?: number, + product_category_id?: number, received_date?: string, po_date?: string, start_date?: string, end_date?: string, + sort_by?: string, + filter_by?: string, page?: number, limit?: number ): Promise | undefined> { @@ -30,10 +33,13 @@ export class LogisticApiService extends BaseApiService< area_id: area_id, supplier_id: supplier_id, product_id: product_id, + product_category_id: product_category_id, received_date: received_date, po_date: po_date, start_date: start_date, end_date: end_date, + sort_by: sort_by, + filter_by: filter_by, page: page, limit: limit, }, From 2a00da0298a61ff19d5574aeba2dcd65c5d8d5a2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 11:12:30 +0700 Subject: [PATCH 30/56] refactor(FE-361): Make filter layout responsive --- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 4144ae94..c8a57fd7 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -492,7 +492,7 @@ const PurchasesPerSupplierTab = () => {
-
+
{ isClearable />
-
+
{ isLoading={false} isClearable={false} /> -
+
Date: Tue, 16 Dec 2025 12:00:56 +0700 Subject: [PATCH 31/56] feat(FE-364): Add PDF export for purchases per supplier --- .../export/PurchasesPerSupplierExport.tsx | 493 ++++++++++++++++++ .../tab/PurchasesPerSupplierTab.tsx | 127 ++++- 2 files changed, 615 insertions(+), 5 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index e69de29b..b96b093a 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -0,0 +1,493 @@ +'use client'; + +import { + Page, + Text, + View, + Document, + StyleSheet, + Font, + pdf, +} from '@react-pdf/renderer'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; +import { formatDate, formatNumber } from '@/lib/helper'; + +Font.register({ + family: 'Helvetica', + src: 'helvetica', +}); + +const pdfStyles = StyleSheet.create({ + page: { + fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', + }, + header: { + marginBottom: 20, + }, + logo: { + width: 120, + height: 30, + marginBottom: 8, + }, + companyInfo: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 4, + color: '#1f74bf', + }, + address: { + fontSize: 8, + color: '#666666', + maxWidth: 400, + marginBottom: 10, + }, + divider: { + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + marginBottom: 15, + }, + titleSection: { + marginBottom: 10, + }, + mainTitle: { + fontSize: 14, + fontWeight: 'bold', + marginBottom: 5, + color: '#1f74bf', + }, + parameterSection: { + fontSize: 9, + color: '#666666', + marginBottom: 15, + }, + supplierTitle: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 8, + color: '#1f74bf', + }, + table: { + borderWidth: 1, + borderColor: '#000000', + marginBottom: 15, + }, + tableRow: { + flexDirection: 'row', + }, + tableHeader: { + backgroundColor: '#F5F5F5', + }, + tableCell: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + }, + tableCellLast: { + flex: 1, + padding: 4, + fontSize: 8, + }, + tableCellHeader: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + }, + tableCellHeaderCenter: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + textAlign: 'center', + }, + tableCellHeaderRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + textAlign: 'right', + }, + tableCellHeaderRightLast: { + flex: 1, + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + textAlign: 'right', + }, + tableCellHeaderLast: { + flex: 1, + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + }, + tableCellHeaderCenterLast: { + flex: 1, + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + textAlign: 'center', + }, + tableCellRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + textAlign: 'right', + }, + tableCellCenter: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + textAlign: 'center', + }, + tableCellRightLast: { + flex: 1, + padding: 4, + fontSize: 8, + textAlign: 'right', + }, + tableCellCenterLast: { + flex: 1, + padding: 4, + fontSize: 8, + textAlign: 'center', + }, + tableBorderBottom: { + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + }, + totalRow: { + backgroundColor: '#F5F5F5', + borderTopWidth: 1, + borderTopColor: '#000000', + borderTopStyle: 'solid', + }, + totalCell: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + fontWeight: 'bold', + }, + totalCellRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + fontWeight: 'bold', + textAlign: 'right', + }, + totalCellRightLast: { + flex: 1, + padding: 4, + fontSize: 8, + fontWeight: 'bold', + textAlign: 'right', + }, + totalCellLast: { + flex: 1, + padding: 4, + fontSize: 8, + fontWeight: 'bold', + }, + supplierSection: { + marginBottom: 20, + }, + supplierSectionBreak: { + marginBottom: 25, + }, + badge: { + backgroundColor: '#1f74bf', + color: '#FFFFFF', + padding: 2, + borderRadius: 2, + fontSize: 7, + fontWeight: 'bold', + alignSelf: 'flex-start', + }, +}); + +interface PurchasesPerSupplierExportParams { + data: LogisticPurchasePerSupplierReport['rows']; + params: { + area_name?: string; + supplier_name?: string; + product_name?: string; + product_category_name?: string; + received_date?: string; + po_date?: string; + start_date?: string; + end_date?: string; + sort_by?: string; + filter_by?: string; + }; +} + +interface GroupedSupplierData { + id: number; + supplier: LogisticPurchasePerSupplierReport['rows'][number]['supplier']; + items: LogisticPurchasePerSupplierReport['rows'][number][]; +} + +const groupDataBySupplier = ( + data: LogisticPurchasePerSupplierReport['rows'] +): GroupedSupplierData[] => { + const groups: { + [key: number]: GroupedSupplierData; + } = {}; + + data.forEach((item) => { + const supplierId = item.supplier?.id; + if (supplierId && !groups[supplierId]) { + groups[supplierId] = { + id: supplierId, + supplier: item.supplier, + items: [], + }; + } + if (groups[supplierId]) { + groups[supplierId].items.push(item); + } + }); + + return Object.values(groups) as GroupedSupplierData[]; +}; + +const getParameterText = ( + params: PurchasesPerSupplierExportParams['params'] +) => { + const paramsText = []; + + if (params.filter_by === 'received_date') { + paramsText.push('Tanggal Terima'); + } else if (params.filter_by === 'po_date') { + paramsText.push('Tanggal PO'); + } + + if (params.supplier_name) { + paramsText.push(`Supplier: ${params.supplier_name}`); + } else { + paramsText.push('Semua Supplier'); + } + + if (params.start_date && params.end_date) { + const startDate = formatDate(params.start_date, 'DD MMM YYYY'); + const endDate = formatDate(params.end_date, 'DD MMM YYYY'); + paramsText.push(`Periode: ${startDate} - ${endDate}`); + } else if (params.start_date) { + const startDate = formatDate(params.start_date, 'DD MMM YYYY'); + paramsText.push(`Tanggal: ${startDate}`); + } + + const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm'); + paramsText.push(`Dicetak: ${currentDate}`); + + return paramsText.join(' | '); +}; + +const createPDFDocument = ( + groupedData: GroupedSupplierData[], + params: PurchasesPerSupplierExportParams['params'] +) => ( + + + {/* Title and Parameters */} + + + Laporan > Rekapitulasi Pembelian Per Supplier + + + Jenis Tanggal:{' '} + {params.filter_by === 'received_date' + ? 'Tanggal Terima' + : 'Tanggal PO'}{' '} + | {getParameterText(params)} + + + + {/* Supplier Sections */} + {groupedData.map( + (supplierGroup: GroupedSupplierData, supplierIndex: number) => { + return ( + + + {supplierGroup.supplier.name} + + + + {/* Table Header */} + + + No + + + Tanggal Terima + + + Tanggal PO + + + Referensi + + + Produk + + + Tujuan + + + Qty + + + Harga Beli + + + Nilai Pembelian + + + Biaya Transport + + + Total + + + Armada + + + Surat Jalan + + + + {/* Table Body */} + {supplierGroup.items.map( + ( + item: LogisticPurchasePerSupplierReport['rows'][number], + index: number + ) => ( + + + {index + 1} + + + + {formatDate(item.receive_date, 'DD-MMM-YYYY')} + + + + {formatDate(item.po_date, 'DD-MMM-YYYY')} + + + {item.po_number || '-'} + + + {item.product?.name || '-'} + + + {item.warehouse?.name || '-'} + + + {formatNumber(item.qty || 0)} + + + {formatNumber(item.unit_price || 0)} + + + {formatNumber(item.purchase_value || 0)} + + + + {formatNumber(item.transport_unit_price || 0)} + + + + {formatNumber(item.total_amount || 0)} + + + + {item.expedition || '-'} + + + + {item.delivery_number || '-'} + + + ) + )} + + + ); + } + )} + + +); + +export const generatePurchasesPerSupplierPDF = async ( + data: LogisticPurchasePerSupplierReport['rows'], + params: PurchasesPerSupplierExportParams['params'] +): Promise => { + const groupedData = groupDataBySupplier(data); + const PDFDocument = createPDFDocument(groupedData, params); + + try { + const blob = await pdf(PDFDocument).toBlob(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `laporan-pembelian-per-supplier-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + throw error; + } +}; diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index c8a57fd7..ac0f3359 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -23,6 +23,8 @@ import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; +import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport'; +import toast from 'react-hot-toast'; interface Totals { totalQty: number; @@ -34,6 +36,9 @@ interface Totals { } const PurchasesPerSupplierTab = () => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + // ===== PAGINATION STATE ===== const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); @@ -247,13 +252,121 @@ const PurchasesPerSupplierTab = () => { ? purchasePerSupplier.meta : undefined; + const { data: allDataForExport } = useSWR( + isSubmitted + ? () => { + const params = { + area_id: tableFilterState.area_id + ? Number(tableFilterState.area_id) + : undefined, + supplier_id: tableFilterState.supplier_id + ? Number(tableFilterState.supplier_id) + : undefined, + product_id: tableFilterState.product_id + ? Number(tableFilterState.product_id) + : undefined, + product_category_id: tableFilterState.product_category_id + ? Number(tableFilterState.product_category_id) + : undefined, + received_date: + tableFilterState.filter_by === 'received_date' + ? tableFilterState.start_date || undefined + : undefined, + po_date: + tableFilterState.filter_by === 'po_date' + ? tableFilterState.start_date || undefined + : undefined, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + sort_by: tableFilterState.sort_by || undefined, + filter_by: tableFilterState.filter_by || undefined, + limit: 10000, + page: 1, + }; + + return ['logistic-purchase-report-export', params]; + } + : null, + ([, params]) => + LogisticApi.getLogisticStockReport( + params.area_id, + params.supplier_id, + params.product_id, + params.product_category_id, + params.received_date, + params.po_date, + params.start_date, + params.end_date, + params.sort_by, + params.filter_by, + params.page, + params.limit + ) + ); + + const allExportData: LogisticPurchasePerSupplierReport['rows'] = useMemo( + () => + isResponseSuccess(allDataForExport) + ? (allDataForExport?.data + ?.rows as LogisticPurchasePerSupplierReport['rows']) || [] + : [], + [allDataForExport] + ); + const handleExportExcel = useCallback(() => { - alert('Export to Excel functionality to be implemented.'); + toast.error('Export to Excel functionality will be implemented.'); }, []); - const handleExportPdf = useCallback(() => { - alert('Export to PDF functionality to be implemented.'); - }, []); + const handleExportPdf = useCallback(async () => { + if (allExportData.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + setIsPdfExportLoading(true); + try { + const exportParams = { + area_name: tableFilterState.area_id + ? areaOptions.find( + (opt) => opt.value === Number(tableFilterState.area_id) + )?.label || '' + : 'Semua Area', + supplier_name: tableFilterState.supplier_id + ? supplierOptions.find( + (opt) => opt.value === Number(tableFilterState.supplier_id) + )?.label || '' + : 'Semua Supplier', + product_name: tableFilterState.product_id + ? productOptions.find( + (opt) => opt.value === Number(tableFilterState.product_id) + )?.label || '' + : 'Semua Produk', + product_category_name: tableFilterState.product_category_id + ? productCategoryOptions.find( + (opt) => + opt.value === Number(tableFilterState.product_category_id) + )?.label || '' + : 'Semua Kategori Produk', + filter_by: tableFilterState.filter_by || 'received_date', + start_date: tableFilterState.start_date || '', + end_date: tableFilterState.end_date || '', + }; + + await generatePurchasesPerSupplierPDF(allExportData, exportParams); + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [ + allExportData, + tableFilterState, + areaOptions, + supplierOptions, + productOptions, + productCategoryOptions, + ]); // ===== PAGINATION HANDLERS ===== const handlePageChange = (page: number) => { @@ -483,7 +596,11 @@ const PurchasesPerSupplierTab = () => { Reset Export} + trigger={ + + } align='end' > From 0d8e642b4eba62eeade2ccd6a48a052377ea3551 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 13:26:16 +0700 Subject: [PATCH 32/56] refactor(FE-364): update PDF export layout and parameter badges --- .../export/PurchasesPerSupplierExport.tsx | 80 ++++++++----------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index b96b093a..c16b261d 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -24,32 +24,6 @@ const pdfStyles = StyleSheet.create({ padding: 20, backgroundColor: '#FFFFFF', }, - header: { - marginBottom: 20, - }, - logo: { - width: 120, - height: 30, - marginBottom: 8, - }, - companyInfo: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 4, - color: '#1f74bf', - }, - address: { - fontSize: 8, - color: '#666666', - maxWidth: 400, - marginBottom: 10, - }, - divider: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - marginBottom: 15, - }, titleSection: { marginBottom: 10, }, @@ -59,11 +33,6 @@ const pdfStyles = StyleSheet.create({ marginBottom: 5, color: '#1f74bf', }, - parameterSection: { - fontSize: 9, - color: '#666666', - marginBottom: 15, - }, supplierTitle: { fontSize: 12, fontWeight: 'bold', @@ -223,10 +192,10 @@ const pdfStyles = StyleSheet.create({ fontWeight: 'bold', }, supplierSection: { - marginBottom: 20, + marginBottom: 10, }, supplierSectionBreak: { - marginBottom: 25, + marginBottom: 15, }, badge: { backgroundColor: '#1f74bf', @@ -236,6 +205,21 @@ const pdfStyles = StyleSheet.create({ fontSize: 7, fontWeight: 'bold', alignSelf: 'flex-start', + marginRight: 4, + }, + parameterBadge: { + backgroundColor: '#F5F5F5', + color: '#333333', + padding: 4, + borderRadius: 4, + fontSize: 8, + marginRight: 8, + marginBottom: 4, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, }, }); @@ -290,12 +274,6 @@ const getParameterText = ( ) => { const paramsText = []; - if (params.filter_by === 'received_date') { - paramsText.push('Tanggal Terima'); - } else if (params.filter_by === 'po_date') { - paramsText.push('Tanggal PO'); - } - if (params.supplier_name) { paramsText.push(`Supplier: ${params.supplier_name}`); } else { @@ -314,7 +292,7 @@ const getParameterText = ( const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm'); paramsText.push(`Dicetak: ${currentDate}`); - return paramsText.join(' | '); + return paramsText; }; const createPDFDocument = ( @@ -328,13 +306,21 @@ const createPDFDocument = ( Laporan > Rekapitulasi Pembelian Per Supplier - - Jenis Tanggal:{' '} - {params.filter_by === 'received_date' - ? 'Tanggal Terima' - : 'Tanggal PO'}{' '} - | {getParameterText(params)} - + + + + Jenis Tanggal:{' '} + {params.filter_by === 'received_date' + ? 'Tanggal Terima' + : 'Tanggal PO'} + + + {getParameterText(params).map((param, index) => ( + + {param} + + ))} + {/* Supplier Sections */} From 6f18c580428329a2601f1e8b4bec6d955a6e7b4e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 13:33:57 +0700 Subject: [PATCH 33/56] refactor(FE-364): refactor PDF table styles and remove total styles --- .../export/PurchasesPerSupplierExport.tsx | 74 ++++--------------- 1 file changed, 14 insertions(+), 60 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index c16b261d..0281b22f 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -57,6 +57,7 @@ const pdfStyles = StyleSheet.create({ borderRightStyle: 'solid', padding: 4, fontSize: 8, + textAlign: 'center', }, tableCellLast: { flex: 1, @@ -72,6 +73,11 @@ const pdfStyles = StyleSheet.create({ fontSize: 8, fontWeight: 'bold', backgroundColor: '#F5F5F5', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + textAlign: 'center', }, tableCellHeaderCenter: { flex: 1, @@ -94,14 +100,10 @@ const pdfStyles = StyleSheet.create({ fontWeight: 'bold', backgroundColor: '#F5F5F5', textAlign: 'right', - }, - tableCellHeaderRightLast: { - flex: 1, - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'right', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, }, tableCellHeaderLast: { flex: 1, @@ -109,14 +111,10 @@ const pdfStyles = StyleSheet.create({ fontSize: 8, fontWeight: 'bold', backgroundColor: '#F5F5F5', - }, - tableCellHeaderCenterLast: { - flex: 1, - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'center', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, }, tableCellRight: { flex: 1, @@ -136,12 +134,6 @@ const pdfStyles = StyleSheet.create({ fontSize: 8, textAlign: 'center', }, - tableCellRightLast: { - flex: 1, - padding: 4, - fontSize: 8, - textAlign: 'right', - }, tableCellCenterLast: { flex: 1, padding: 4, @@ -153,44 +145,6 @@ const pdfStyles = StyleSheet.create({ borderBottomColor: '#000000', borderBottomStyle: 'solid', }, - totalRow: { - backgroundColor: '#F5F5F5', - borderTopWidth: 1, - borderTopColor: '#000000', - borderTopStyle: 'solid', - }, - totalCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - }, - totalCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - textAlign: 'right', - }, - totalCellRightLast: { - flex: 1, - padding: 4, - fontSize: 8, - fontWeight: 'bold', - textAlign: 'right', - }, - totalCellLast: { - flex: 1, - padding: 4, - fontSize: 8, - fontWeight: 'bold', - }, supplierSection: { marginBottom: 10, }, From 9ba3fa1b6cddb1a92dc0d878a687a273e66ccfcb Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 13:34:36 +0700 Subject: [PATCH 34/56] refactor(FE-364): refactor PDF table styles and remove total styles --- .../export/PurchasesPerSupplierExport.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index 0281b22f..6ba54396 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -79,17 +79,6 @@ const pdfStyles = StyleSheet.create({ paddingVertical: 12, textAlign: 'center', }, - tableCellHeaderCenter: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'center', - }, tableCellHeaderRight: { flex: 1, borderRightWidth: 1, From 5c8bc4fc6e1505ba13adc8e421f540ed9f480ac1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 13:52:55 +0700 Subject: [PATCH 35/56] feat(FE-364): Center align badge in PDF export styles --- .../report/logistic-stock/export/PurchasesPerSupplierExport.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index 6ba54396..5ed06e84 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -147,7 +147,7 @@ const pdfStyles = StyleSheet.create({ borderRadius: 2, fontSize: 7, fontWeight: 'bold', - alignSelf: 'flex-start', + alignSelf: 'center', marginRight: 4, }, parameterBadge: { From 84e562e22c5cc64dd70a71b5b1b9adf650568a09 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 13:56:48 +0700 Subject: [PATCH 36/56] refactor(FE-364): Refine PDF table cell styles and header alignment --- .../export/PurchasesPerSupplierExport.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index 5ed06e84..cd4603ba 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -51,6 +51,15 @@ const pdfStyles = StyleSheet.create({ backgroundColor: '#F5F5F5', }, tableCell: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + textAlign: 'left', + }, + tableCellNo: { flex: 1, borderRightWidth: 1, borderRightColor: '#000000', @@ -104,6 +113,7 @@ const pdfStyles = StyleSheet.create({ borderBottomColor: '#000000', borderBottomStyle: 'solid', paddingVertical: 12, + textAlign: 'center', }, tableCellRight: { flex: 1, @@ -342,7 +352,7 @@ const createPDFDocument = ( : {}, ]} > - + {index + 1} From c9544e1bd07be46eb44a9402ab1dee1a00e5403f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 14:21:58 +0700 Subject: [PATCH 37/56] refactor(FE-361): Format quantity with formatNumber --- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index ac0f3359..0762352a 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -14,7 +14,7 @@ import { ProductCategoryApi } from '@/services/api/master-data'; import { LogisticApi } from '@/services/api/logistic'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate } from '@/lib/helper'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -482,11 +482,11 @@ const PurchasesPerSupplierTab = () => { accessorKey: 'qty', cell: (props) => { const value = props.row.original.qty; - return
{value.toLocaleString()}
; + return
{formatNumber(value)}
; }, footer: () => (
- {totals.totalQty.toLocaleString()} + {formatNumber(totals.totalQty)}
), }, From d17c11e2f259f5f548335ed7f8586886d66ba2b3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 14:48:17 +0700 Subject: [PATCH 38/56] feat(FE-364): Implement Excel export for purchases per supplier --- .../tab/PurchasesPerSupplierTab.tsx | 135 +++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 0762352a..75ac0a57 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -25,6 +25,7 @@ import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport'; import toast from 'react-hot-toast'; +import * as XLSX from 'xlsx'; interface Totals { totalQty: number; @@ -314,8 +315,138 @@ const PurchasesPerSupplierTab = () => { ); const handleExportExcel = useCallback(() => { - toast.error('Export to Excel functionality will be implemented.'); - }, []); + if (allExportData.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + try { + const groupedBySupplier: { [key: string]: typeof allExportData } = {}; + + allExportData.forEach((item) => { + const supplierName = item.supplier?.name || 'Unknown Supplier'; + if (!groupedBySupplier[supplierName]) { + groupedBySupplier[supplierName] = []; + } + groupedBySupplier[supplierName].push(item); + }); + + const workbook = XLSX.utils.book_new(); + + const areaName = tableFilterState.area_id + ? areaOptions.find( + (opt) => opt.value === Number(tableFilterState.area_id) + )?.label || 'Semua Area' + : 'Semua Area'; + + const supplierName = tableFilterState.supplier_id + ? supplierOptions.find( + (opt) => opt.value === Number(tableFilterState.supplier_id) + )?.label || 'Semua Supplier' + : 'Semua Supplier'; + + const startDate = tableFilterState.start_date || 'all'; + const endDate = tableFilterState.end_date || 'all'; + + Object.entries(groupedBySupplier).forEach( + ([supplierName, supplierData]) => { + const totals = supplierData.reduce( + (acc, item) => ({ + totalQty: acc.totalQty + (item.qty || 0), + totalPrice: acc.totalPrice + (item.unit_price || 0), + totalPurchaseAmount: + acc.totalPurchaseAmount + (item.purchase_value || 0), + totalTransport: + acc.totalTransport + (item.transport_unit_price || 0), + totalValueTransport: + acc.totalValueTransport + (item.transport_value || 0), + totalJumlah: acc.totalJumlah + (item.total_amount || 0), + }), + { + totalQty: 0, + totalPrice: 0, + totalPurchaseAmount: 0, + totalTransport: 0, + totalValueTransport: 0, + totalJumlah: 0, + } + ); + + const excelData: { [key: string]: string | number }[] = + supplierData.map((item, index) => ({ + No: index + 1, + 'Tanggal Terima': item.receive_date + ? formatDate(item.receive_date, 'DD MMM YYYY') + : '', + 'Tanggal PO': item.po_date + ? formatDate(item.po_date, 'DD MMM YYYY') + : '', + 'No. Referensi': item.po_number || '', + 'Nama Produk': item.product?.name || '', + Tujuan: item.warehouse?.name || '', + QTY: item.qty || 0, + 'Harga Beli (Rp)': item.unit_price || 0, + 'Value Harga Beli (Rp)': item.purchase_value || 0, + 'Transport (Rp)': item.transport_unit_price || 0, + 'Value Transport (Rp)': item.transport_value || 0, + 'Jumlah (Rp)': item.total_amount || 0, + Ekspedisi: item.expedition || '', + 'Surat Jalan': item.delivery_number || '', + })); + + excelData.push({ + No: 'Total', + 'Tanggal Terima': '', + 'Tanggal PO': '', + 'No. Referensi': '', + 'Nama Produk': '', + Tujuan: '', + QTY: totals.totalQty, + 'Harga Beli (Rp)': totals.totalPrice, + 'Value Harga Beli (Rp)': totals.totalPurchaseAmount, + 'Transport (Rp)': totals.totalTransport, + 'Value Transport (Rp)': totals.totalValueTransport, + 'Jumlah (Rp)': totals.totalJumlah, + Ekspedisi: '', + 'Surat Jalan': '', + }); + + const worksheet = XLSX.utils.json_to_sheet(excelData); + + const colWidths = [ + { wch: 5 }, // No + { wch: 15 }, // Tanggal Terima + { wch: 15 }, // Tanggal PO + { wch: 15 }, // No. Referensi + { wch: 30 }, // Nama Produk + { wch: 20 }, // Tujuan + { wch: 10 }, // QTY + { wch: 18 }, // Harga Beli + { wch: 20 }, // Value Harga Beli + { wch: 15 }, // Transport + { wch: 20 }, // Value Transport + { wch: 18 }, // Jumlah + { wch: 15 }, // Ekspedisi + { wch: 15 }, // Surat Jalan + ]; + worksheet['!cols'] = colWidths; + + const sheetName = + supplierName.length > 31 + ? supplierName.substring(0, 31) + : supplierName; + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + } + ); + + const filename = `Laporan_Pembelian_Per_Supplier_${areaName}_${supplierName}_${startDate}_to_${endDate}.xlsx`; + + XLSX.writeFile(workbook, filename); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } + }, [allExportData, tableFilterState, areaOptions, supplierOptions]); const handleExportPdf = useCallback(async () => { if (allExportData.length === 0) { From c04cd29ac74fb8e3e3f8cd1a9d0f295dfb48a554 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 16 Dec 2025 15:19:47 +0700 Subject: [PATCH 39/56] refactor(FE-363): Use snake_case for totals in purchases tab --- .../tab/PurchasesPerSupplierTab.tsx | 119 +++++++++--------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 75ac0a57..68aa15d1 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -26,14 +26,21 @@ import Menu from '@/components/menu/Menu'; import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport'; import toast from 'react-hot-toast'; import * as XLSX from 'xlsx'; +import { Supplier } from '@/types/api/master-data/supplier'; interface Totals { - totalQty: number; - totalPrice: number; - totalPurchaseAmount: number; - totalTransport: number; - totalValueTransport: number; - totalJumlah: number; + total_qty: number; + total_price: number; + total_purchase_amount: number; + total_transport: number; + total_value_transport: number; + total_jumlah: number; +} + +interface GroupedSupplierData { + id: number; + supplier: Supplier; + items: LogisticPurchasePerSupplierReport['rows']; } const PurchasesPerSupplierTab = () => { @@ -314,6 +321,7 @@ const PurchasesPerSupplierTab = () => { [allDataForExport] ); + // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(() => { if (allExportData.length === 0) { toast.error('Tidak ada data untuk diekspor.'); @@ -352,23 +360,23 @@ const PurchasesPerSupplierTab = () => { ([supplierName, supplierData]) => { const totals = supplierData.reduce( (acc, item) => ({ - totalQty: acc.totalQty + (item.qty || 0), - totalPrice: acc.totalPrice + (item.unit_price || 0), - totalPurchaseAmount: - acc.totalPurchaseAmount + (item.purchase_value || 0), - totalTransport: - acc.totalTransport + (item.transport_unit_price || 0), - totalValueTransport: - acc.totalValueTransport + (item.transport_value || 0), - totalJumlah: acc.totalJumlah + (item.total_amount || 0), + total_qty: acc.total_qty + (item.qty || 0), + total_price: acc.total_price + (item.unit_price || 0), + total_purchase_amount: + acc.total_purchase_amount + (item.purchase_value || 0), + total_transport: + acc.total_transport + (item.transport_unit_price || 0), + total_value_transport: + acc.total_value_transport + (item.transport_value || 0), + total_jumlah: acc.total_jumlah + (item.total_amount || 0), }), { - totalQty: 0, - totalPrice: 0, - totalPurchaseAmount: 0, - totalTransport: 0, - totalValueTransport: 0, - totalJumlah: 0, + total_qty: 0, + total_price: 0, + total_purchase_amount: 0, + total_transport: 0, + total_value_transport: 0, + total_jumlah: 0, } ); @@ -401,12 +409,12 @@ const PurchasesPerSupplierTab = () => { 'No. Referensi': '', 'Nama Produk': '', Tujuan: '', - QTY: totals.totalQty, - 'Harga Beli (Rp)': totals.totalPrice, - 'Value Harga Beli (Rp)': totals.totalPurchaseAmount, - 'Transport (Rp)': totals.totalTransport, - 'Value Transport (Rp)': totals.totalValueTransport, - 'Jumlah (Rp)': totals.totalJumlah, + QTY: totals.total_qty, + 'Harga Beli (Rp)': totals.total_price, + 'Value Harga Beli (Rp)': totals.total_purchase_amount, + 'Transport (Rp)': totals.total_transport, + 'Value Transport (Rp)': totals.total_value_transport, + 'Jumlah (Rp)': totals.total_jumlah, Ekspedisi: '', 'Surat Jalan': '', }); @@ -514,15 +522,13 @@ const PurchasesPerSupplierTab = () => { } }; - interface GroupedSupplierData { - id: number; - supplier: { - id: number; - name: string; - }; - items: LogisticPurchasePerSupplierReport['rows']; - } + const handlePrevPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + // ===== TABLE COLUMNS DEFINITION ===== const groupedData = useMemo(() => { const groups: { [key: number]: GroupedSupplierData } = {}; @@ -542,13 +548,6 @@ const PurchasesPerSupplierTab = () => { return Object.values(groups); }, [data]); - - const handlePrevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; - const getTableColumns = ( totals: Totals ): ColumnDef[] => { @@ -617,7 +616,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatNumber(totals.totalQty)} + {formatNumber(totals.total_qty)}
), }, @@ -631,7 +630,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.totalPrice)} + {formatCurrency(totals.total_price)}
), }, @@ -645,7 +644,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.totalPurchaseAmount)} + {formatCurrency(totals.total_purchase_amount)}
), }, @@ -659,7 +658,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.totalTransport)} + {formatCurrency(totals.total_transport)}
), }, @@ -673,7 +672,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.totalValueTransport)} + {formatCurrency(totals.total_value_transport)}
), }, @@ -687,7 +686,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.totalJumlah)} + {formatCurrency(totals.total_jumlah)}
), }, @@ -853,37 +852,37 @@ const PurchasesPerSupplierTab = () => {
) : ( groupedData.map((supplier) => { - const totalQty = supplier.items.reduce( + const total_qty = supplier.items.reduce( (sum, item) => sum + (item.qty || 0), 0 ); - const totalPrice = supplier.items.reduce( + const total_price = supplier.items.reduce( (sum, item) => sum + (item.purchase_value || 0), 0 ); - const totalTransport = supplier.items.reduce( + const total_transport = supplier.items.reduce( (sum, item) => sum + (item.transport_value || 0), 0 ); - const totalValueTransport = supplier.items.reduce( + const total_value_transport = supplier.items.reduce( (sum, item) => sum + (item.transport_value || 0), 0 ); - const totalJumlah = supplier.items.reduce( + const total_jumlah = supplier.items.reduce( (sum, item) => sum + (item.total_amount || 0), 0 ); const totals = { - totalQty, - totalPrice, - totalPurchaseAmount: totalPrice, - totalTransport, - totalValueTransport, - totalJumlah, + total_qty, + total_price, + total_purchase_amount: total_price, + total_transport, + total_value_transport, + total_jumlah, }; - const totalPurchase = totals.totalJumlah; + const totalPurchase = totals.total_jumlah; const tableColumns = getTableColumns(totals); return ( From 9f521a6a0883b656aff1795b5d39a124c1303698 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 10:00:35 +0700 Subject: [PATCH 40/56] refactor(FE-363): Rename logistic stock report API method --- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 4 ++-- src/services/api/logistic.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 68aa15d1..6c59f697 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -230,7 +230,7 @@ const PurchasesPerSupplierTab = () => { } : null, ([, params]) => - LogisticApi.getLogisticStockReport( + LogisticApi.getLogisticPurchasePerSupplierReport( params.area_id, params.supplier_id, params.product_id, @@ -296,7 +296,7 @@ const PurchasesPerSupplierTab = () => { } : null, ([, params]) => - LogisticApi.getLogisticStockReport( + LogisticApi.getLogisticPurchasePerSupplierReport( params.area_id, params.supplier_id, params.product_id, diff --git a/src/services/api/logistic.ts b/src/services/api/logistic.ts index 64930e19..077cfcb9 100644 --- a/src/services/api/logistic.ts +++ b/src/services/api/logistic.ts @@ -11,7 +11,7 @@ export class LogisticApiService extends BaseApiService< super(basePath); } - async getLogisticStockReport( + async getLogisticPurchasePerSupplierReport( area_id?: number, supplier_id?: number, product_id?: number, From 01313f0b0923ffb6d5fe6c5a2ffcfba1ac3a8c3e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 10:05:32 +0700 Subject: [PATCH 41/56] refactor(FE-363): Move logistic API to report/logistic-stock --- .../pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 2 +- src/services/api/{logistic.ts => report/logistic-stock.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/services/api/{logistic.ts => report/logistic-stock.ts} (100%) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 6c59f697..349de06b 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -11,7 +11,7 @@ import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; import { ProductCategoryApi } from '@/services/api/master-data'; -import { LogisticApi } from '@/services/api/logistic'; +import { LogisticApi } from '@/services/api/report/logistic-stock'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; diff --git a/src/services/api/logistic.ts b/src/services/api/report/logistic-stock.ts similarity index 100% rename from src/services/api/logistic.ts rename to src/services/api/report/logistic-stock.ts From c1e075b1ff98b2a657c7ba0e5d380c1625ba09ab Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 11:47:12 +0700 Subject: [PATCH 42/56] refactor(FE): refactor Dropdown component API and Navbar usage --- src/components/Dropdown.tsx | 114 ++++++++++++++++++++++++++ src/components/Navbar.tsx | 11 ++- src/components/dropdown/Dropdown.tsx | 116 --------------------------- 3 files changed, 121 insertions(+), 120 deletions(-) create mode 100644 src/components/Dropdown.tsx delete mode 100644 src/components/dropdown/Dropdown.tsx diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx new file mode 100644 index 00000000..5bfa7a7d --- /dev/null +++ b/src/components/Dropdown.tsx @@ -0,0 +1,114 @@ +import React, { ReactNode, useState, useRef } from 'react'; + +import { cn } from '@/lib/helper'; + +export interface DropdownProps { + trigger: ReactNode; + children: ReactNode; + className?: { + wrapper?: string; + trigger?: string; + content?: string; + }; + align?: 'start' | 'center' | 'end'; + direction?: 'top' | 'bottom' | 'left' | 'right'; + hover?: boolean; + defaultOpen?: boolean; + open?: boolean; + close?: boolean; + controlled?: boolean; +} + +const Dropdown = ({ + trigger, + children, + className, + align, + direction, + hover, + defaultOpen = false, + open, + close, + controlled = false, +}: DropdownProps) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + const dropdownRef = useRef(null); + + const toggleDropdown = () => { + if (!controlled) { + const newState = !isOpen; + setIsOpen(newState); + } + }; + + const getWrapperClasses = () => { + const openState = controlled ? open : isOpen; + + return cn( + 'dropdown', + { + 'dropdown-start': align === 'start', + 'dropdown-center': align === 'center', + 'dropdown-end': align === 'end', + 'dropdown-top': direction === 'top', + 'dropdown-bottom': direction === 'bottom', + 'dropdown-left': direction === 'left', + 'dropdown-right': direction === 'right', + 'dropdown-hover': hover, + 'dropdown-open': openState && !close, + 'dropdown-close': close, + }, + className?.wrapper + ); + }; + + const getTriggerClasses = () => { + return cn(className?.trigger); + }; + + const getContentClasses = () => { + return cn( + 'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box', + className?.content + ); + }; + + if (controlled) { + return ( +
+ {trigger} + {open && !close && ( +
+ {children} +
+ )} +
+ ); + } + + return ( +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleDropdown(); + } + }} + > + {trigger} +
+ {!close && ( +
+ {children} +
+ )} +
+ ); +}; + +export default Dropdown; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index bee92a57..280217a0 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -7,7 +7,7 @@ import { Icon } from '@iconify/react'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Button from '@/components/Button'; -import Dropdown from '@/components/dropdown/Dropdown'; +import Dropdown from '@/components/Dropdown'; import { useAuth } from '@/services/hooks/useAuth'; import { AuthApi } from '@/services/api/auth'; @@ -54,7 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
@@ -62,9 +63,11 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
} - contentClassName='w-52 mt-3' + className={{ + content: 'w-52 mt-3', + }} > - + diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx deleted file mode 100644 index 4489231d..00000000 --- a/src/components/dropdown/Dropdown.tsx +++ /dev/null @@ -1,116 +0,0 @@ -'use client'; - -import { ReactNode, useRef, useEffect, useState } from 'react'; -import { cn } from '@/lib/helper'; - -interface DropdownProps { - trigger: ReactNode; - children: ReactNode; - position?: - | 'top' - | 'bottom' - | 'left' - | 'right' - | 'top-start' - | 'top-end' - | 'bottom-start' - | 'bottom-end' - | 'left-start' - | 'left-end' - | 'right-start' - | 'right-end'; - align?: 'start' | 'center' | 'end'; - hover?: boolean; - className?: string; - contentClassName?: string; -} - -const Dropdown = ({ - trigger, - children, - position = 'bottom', - align = 'start', - hover = false, - className, - contentClassName, -}: DropdownProps) => { - const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); - - // Handle click outside to close dropdown - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setIsOpen(false); - } - }; - - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen]); - - // Build position classes - const getPositionClasses = () => { - const classes: string[] = []; - - // Handle combined positions like 'top-start' - if (position.includes('-')) { - const [pos, al] = position.split('-'); - classes.push(`dropdown-${pos}`); - classes.push(`dropdown-${al}`); - } else { - classes.push(`dropdown-${position}`); - if (align !== 'start') { - classes.push(`dropdown-${align}`); - } - } - - return classes.join(' '); - }; - - const handleToggle = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - // alert('clicked'); - setIsOpen(!isOpen); - }; - - return ( -
- {/* Trigger Button */} -
- {trigger} -
- - {/* Dropdown Content - Only render when open */} - {isOpen && ( -
setIsOpen(false)} // Close on item click - > - {children} -
- )} -
- ); -}; - -export default Dropdown; From 7c9f68d3a3febc9349eb2dd3a147601e3e3687b6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 14:44:36 +0700 Subject: [PATCH 43/56] feat(FE-363): Add Excel export loading and combine export state --- .../logistic-stock/tab/PurchasesPerSupplierTab.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 349de06b..c0672cab 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -46,6 +46,8 @@ interface GroupedSupplierData { const PurchasesPerSupplierTab = () => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; // ===== PAGINATION STATE ===== const [currentPage, setCurrentPage] = useState(1); @@ -256,9 +258,9 @@ const PurchasesPerSupplierTab = () => { ); const meta = - isResponseSuccess(purchasePerSupplier) && 'meta' in purchasePerSupplier + isResponseSuccess(purchasePerSupplier) && purchasePerSupplier?.meta ? purchasePerSupplier.meta - : undefined; + : null; const { data: allDataForExport } = useSWR( isSubmitted @@ -327,6 +329,7 @@ const PurchasesPerSupplierTab = () => { toast.error('Tidak ada data untuk diekspor.'); return; } + setIsExcelExportLoading(true); try { const groupedBySupplier: { [key: string]: typeof allExportData } = {}; @@ -453,6 +456,8 @@ const PurchasesPerSupplierTab = () => { toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); } }, [allExportData, tableFilterState, areaOptions, supplierOptions]); @@ -727,7 +732,7 @@ const PurchasesPerSupplierTab = () => { + } From 1de98db4baf3f11e5ee838058075ca1c8dd52a21 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 11:31:05 +0700 Subject: [PATCH 44/56] refactor(FE-361): Hide unused logistic tabs and adjust card --- .../logistic-stock/LogisticStockTabs.tsx | 20 +++++++++---------- .../tab/PurchasesPerSupplierTab.tsx | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index a3a3b062..1e2d2824 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -10,16 +10,16 @@ const LogisticStockTabs = () => { label: 'Rekapitulasi Pembelian Per Supplier', content: , }, - { - id: '2', - label: 'Rekapitulasi Pemakaian Barang', - content: 'Rekapitulasi Pemakaian Barang Tab', - }, - { - id: '3', - label: 'Rekapitulasi Stock Persediaan Barang', - content: 'Rekapitulasi Stock Persediaan Barang Tab', - }, + // { + // id: '2', + // label: 'Rekapitulasi Pemakaian Barang', + // content: 'Rekapitulasi Pemakaian Barang Tab', + // }, + // { + // id: '3', + // label: 'Rekapitulasi Stock Persediaan Barang', + // content: 'Rekapitulasi Stock Persediaan Barang Tab', + // }, ]; return ( diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index c0672cab..aa6c38b5 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -895,7 +895,8 @@ const PurchasesPerSupplierTab = () => { key={supplier.id} title={supplier.supplier.name} subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`} - className={{ wrapper: 'w-full', body: 'py-6! p-0' }} + className={{ wrapper: 'w-full' }} + variant='bordered' collapsible={true} >
Date: Thu, 18 Dec 2025 13:28:00 +0700 Subject: [PATCH 45/56] refactor(FE-364): Enable multi-select filters for purchases report --- .../tab/PurchasesPerSupplierTab.tsx | 254 +++++++++++------- src/services/api/report/logistic-stock.ts | 8 +- 2 files changed, 154 insertions(+), 108 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index aa6c38b5..ca713426 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -59,10 +59,10 @@ const PurchasesPerSupplierTab = () => { // ===== TABLE FILTER STATE ===== const { state: tableFilterState, updateFilter } = useTableFilter({ initial: { - area_id: '', - supplier_id: '', - product_id: '', - product_category_id: '', + area_id: [] as string[], + supplier_id: [] as string[], + product_id: [] as string[], + product_category_id: [] as string[], received_date: '', po_date: '', start_date: '', @@ -104,8 +104,11 @@ const PurchasesPerSupplierTab = () => { const areaChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - updateFilter('area_id', newVal?.value ? String(newVal.value) : ''); + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'area_id', + arr.map((v) => String((v as OptionType).value)) + ); setIsSubmitted(false); }, [updateFilter] @@ -113,8 +116,11 @@ const PurchasesPerSupplierTab = () => { const supplierChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - updateFilter('supplier_id', newVal?.value ? String(newVal.value) : ''); + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'supplier_id', + arr.map((v) => String((v as OptionType).value)) + ); setIsSubmitted(false); }, [updateFilter] @@ -122,8 +128,11 @@ const PurchasesPerSupplierTab = () => { const productChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - updateFilter('product_id', newVal?.value ? String(newVal.value) : ''); + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'product_id', + arr.map((v) => String((v as OptionType).value)) + ); setIsSubmitted(false); }, [updateFilter] @@ -131,10 +140,10 @@ const PurchasesPerSupplierTab = () => { const productCategoryChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; + const arr = Array.isArray(val) ? val : val ? [val] : []; updateFilter( 'product_category_id', - newVal?.value ? String(newVal.value) : '' + arr.map((v) => String((v as OptionType).value)) ); setIsSubmitted(false); }, @@ -177,10 +186,10 @@ const PurchasesPerSupplierTab = () => { ); const resetFilters = useCallback(() => { - updateFilter('area_id', ''); - updateFilter('supplier_id', ''); - updateFilter('product_id', ''); - updateFilter('product_category_id', ''); + updateFilter('area_id', []); + updateFilter('supplier_id', []); + updateFilter('product_id', []); + updateFilter('product_category_id', []); updateFilter('received_date', ''); updateFilter('po_date', ''); updateFilter('start_date', ''); @@ -200,18 +209,22 @@ const PurchasesPerSupplierTab = () => { isSubmitted ? () => { const params = { - area_id: tableFilterState.area_id - ? Number(tableFilterState.area_id) - : undefined, - supplier_id: tableFilterState.supplier_id - ? Number(tableFilterState.supplier_id) - : undefined, - product_id: tableFilterState.product_id - ? Number(tableFilterState.product_id) - : undefined, - product_category_id: tableFilterState.product_category_id - ? Number(tableFilterState.product_category_id) - : undefined, + area_id: + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id + : undefined, + supplier_id: + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id + : undefined, + product_id: + tableFilterState.product_id.length > 0 + ? tableFilterState.product_id + : undefined, + product_category_id: + tableFilterState.product_category_id.length > 0 + ? tableFilterState.product_category_id + : undefined, received_date: tableFilterState.filter_by === 'received_date' ? tableFilterState.start_date || undefined @@ -266,18 +279,22 @@ const PurchasesPerSupplierTab = () => { isSubmitted ? () => { const params = { - area_id: tableFilterState.area_id - ? Number(tableFilterState.area_id) - : undefined, - supplier_id: tableFilterState.supplier_id - ? Number(tableFilterState.supplier_id) - : undefined, - product_id: tableFilterState.product_id - ? Number(tableFilterState.product_id) - : undefined, - product_category_id: tableFilterState.product_category_id - ? Number(tableFilterState.product_category_id) - : undefined, + area_id: + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id + : undefined, + supplier_id: + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id + : undefined, + product_id: + tableFilterState.product_id.length > 0 + ? tableFilterState.product_id + : undefined, + product_category_id: + tableFilterState.product_category_id.length > 0 + ? tableFilterState.product_category_id + : undefined, received_date: tableFilterState.filter_by === 'received_date' ? tableFilterState.start_date || undefined @@ -344,17 +361,27 @@ const PurchasesPerSupplierTab = () => { const workbook = XLSX.utils.book_new(); - const areaName = tableFilterState.area_id - ? areaOptions.find( - (opt) => opt.value === Number(tableFilterState.area_id) - )?.label || 'Semua Area' - : 'Semua Area'; + const areaName = + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id + .map( + (id) => + areaOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Area' + : 'Semua Area'; - const supplierName = tableFilterState.supplier_id - ? supplierOptions.find( - (opt) => opt.value === Number(tableFilterState.supplier_id) - )?.label || 'Semua Supplier' - : 'Semua Supplier'; + const supplierName = + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id + .map( + (id) => + supplierOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Supplier' + : 'Semua Supplier'; const startDate = tableFilterState.start_date || 'all'; const endDate = tableFilterState.end_date || 'all'; @@ -469,28 +496,56 @@ const PurchasesPerSupplierTab = () => { setIsPdfExportLoading(true); try { + const areaName = + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id + .map( + (id) => + areaOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Area' + : 'Semua Area'; + + const supplierName = + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id + .map( + (id) => + supplierOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Supplier' + : 'Semua Supplier'; + + const productName = + tableFilterState.product_id.length > 0 + ? tableFilterState.product_id + .map( + (id) => + productOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Produk' + : 'Semua Produk'; + + const productCategoryName = + tableFilterState.product_category_id.length > 0 + ? tableFilterState.product_category_id + .map( + (id) => + productCategoryOptions.find((opt) => opt.value === Number(id)) + ?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Kategori Produk' + : 'Semua Kategori Produk'; + const exportParams = { - area_name: tableFilterState.area_id - ? areaOptions.find( - (opt) => opt.value === Number(tableFilterState.area_id) - )?.label || '' - : 'Semua Area', - supplier_name: tableFilterState.supplier_id - ? supplierOptions.find( - (opt) => opt.value === Number(tableFilterState.supplier_id) - )?.label || '' - : 'Semua Supplier', - product_name: tableFilterState.product_id - ? productOptions.find( - (opt) => opt.value === Number(tableFilterState.product_id) - )?.label || '' - : 'Semua Produk', - product_category_name: tableFilterState.product_category_id - ? productCategoryOptions.find( - (opt) => - opt.value === Number(tableFilterState.product_category_id) - )?.label || '' - : 'Semua Kategori Produk', + area_name: areaName, + supplier_name: supplierName, + product_name: productName, + product_category_name: productCategoryName, filter_by: tableFilterState.filter_by || 'received_date', start_date: tableFilterState.start_date || '', end_date: tableFilterState.end_date || '', @@ -748,15 +803,13 @@ const PurchasesPerSupplierTab = () => { - option.value === Number(tableFilterState.area_id) - ) || null - : null - } + value={areaOptions.filter((opt) => + (tableFilterState.area_id || []) + .map(String) + .includes(String(opt.value)) + )} onChange={areaChangeHandler} isLoading={isLoadingAreas} isClearable @@ -764,15 +817,13 @@ const PurchasesPerSupplierTab = () => { - option.value === Number(tableFilterState.supplier_id) - ) || null - : null - } + value={supplierOptions.filter((opt) => + (tableFilterState.supplier_id || []) + .map(String) + .includes(String(opt.value)) + )} onChange={supplierChangeHandler} isLoading={isLoadingSuppliers} isClearable @@ -780,15 +831,13 @@ const PurchasesPerSupplierTab = () => { - option.value === Number(tableFilterState.product_id) - ) || null - : null - } + value={productOptions.filter((opt) => + (tableFilterState.product_id || []) + .map(String) + .includes(String(opt.value)) + )} onChange={productChangeHandler} isLoading={isLoadingProducts} isClearable @@ -798,16 +847,13 @@ const PurchasesPerSupplierTab = () => { - option.value === - Number(tableFilterState.product_category_id) - ) || null - : null - } + value={productCategoryOptions.filter((opt) => + (tableFilterState.product_category_id || []) + .map(String) + .includes(String(opt.value)) + )} onChange={productCategoryChangeHandler} isLoading={isLoadingProductCategories} isClearable diff --git a/src/services/api/report/logistic-stock.ts b/src/services/api/report/logistic-stock.ts index 077cfcb9..753e0aaf 100644 --- a/src/services/api/report/logistic-stock.ts +++ b/src/services/api/report/logistic-stock.ts @@ -12,10 +12,10 @@ export class LogisticApiService extends BaseApiService< } async getLogisticPurchasePerSupplierReport( - area_id?: number, - supplier_id?: number, - product_id?: number, - product_category_id?: number, + area_id?: string, + supplier_id?: string, + product_id?: string, + product_category_id?: string, received_date?: string, po_date?: string, start_date?: string, From 87adbf8547b307f77e14572ed99523fdedff6ac7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 13:31:08 +0700 Subject: [PATCH 46/56] refactor(FE-363): Accept array filters in logistic purchase report --- src/services/api/report/logistic-stock.ts | 25 +++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/services/api/report/logistic-stock.ts b/src/services/api/report/logistic-stock.ts index 753e0aaf..e09f8bde 100644 --- a/src/services/api/report/logistic-stock.ts +++ b/src/services/api/report/logistic-stock.ts @@ -12,10 +12,10 @@ export class LogisticApiService extends BaseApiService< } async getLogisticPurchasePerSupplierReport( - area_id?: string, - supplier_id?: string, - product_id?: string, - product_category_id?: string, + area_id?: string[], + supplier_id?: string[], + product_id?: string[], + product_category_id?: string[], received_date?: string, po_date?: string, start_date?: string, @@ -30,10 +30,19 @@ export class LogisticApiService extends BaseApiService< >(`purchase-supplier`, { method: 'GET', params: { - area_id: area_id, - supplier_id: supplier_id, - product_id: product_id, - product_category_id: product_category_id, + area_id: area_id && area_id.length > 0 ? area_id.join(',') : undefined, + supplier_id: + supplier_id && supplier_id.length > 0 + ? supplier_id.join(',') + : undefined, + product_id: + product_id && product_id.length > 0 + ? product_id.join(',') + : undefined, + product_category_id: + product_category_id && product_category_id.length > 0 + ? product_category_id.join(',') + : undefined, received_date: received_date, po_date: po_date, start_date: start_date, From 915e68f7554740d201cf1ff7cc8dbfbbc4f8eead Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 13:35:40 +0700 Subject: [PATCH 47/56] refactor(FE-363): Send joined filter params to logistic report API --- .../tab/PurchasesPerSupplierTab.tsx | 16 ++++++------ src/services/api/report/logistic-stock.ts | 25 ++++++------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index ca713426..058cd049 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -211,19 +211,19 @@ const PurchasesPerSupplierTab = () => { const params = { area_id: tableFilterState.area_id.length > 0 - ? tableFilterState.area_id + ? tableFilterState.area_id.join(',') : undefined, supplier_id: tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id + ? tableFilterState.supplier_id.join(',') : undefined, product_id: tableFilterState.product_id.length > 0 - ? tableFilterState.product_id + ? tableFilterState.product_id.join(',') : undefined, product_category_id: tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id + ? tableFilterState.product_category_id.join(',') : undefined, received_date: tableFilterState.filter_by === 'received_date' @@ -281,19 +281,19 @@ const PurchasesPerSupplierTab = () => { const params = { area_id: tableFilterState.area_id.length > 0 - ? tableFilterState.area_id + ? tableFilterState.area_id.join(',') : undefined, supplier_id: tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id + ? tableFilterState.supplier_id.join(',') : undefined, product_id: tableFilterState.product_id.length > 0 - ? tableFilterState.product_id + ? tableFilterState.product_id.join(',') : undefined, product_category_id: tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id + ? tableFilterState.product_category_id.join(',') : undefined, received_date: tableFilterState.filter_by === 'received_date' diff --git a/src/services/api/report/logistic-stock.ts b/src/services/api/report/logistic-stock.ts index e09f8bde..753e0aaf 100644 --- a/src/services/api/report/logistic-stock.ts +++ b/src/services/api/report/logistic-stock.ts @@ -12,10 +12,10 @@ export class LogisticApiService extends BaseApiService< } async getLogisticPurchasePerSupplierReport( - area_id?: string[], - supplier_id?: string[], - product_id?: string[], - product_category_id?: string[], + area_id?: string, + supplier_id?: string, + product_id?: string, + product_category_id?: string, received_date?: string, po_date?: string, start_date?: string, @@ -30,19 +30,10 @@ export class LogisticApiService extends BaseApiService< >(`purchase-supplier`, { method: 'GET', params: { - area_id: area_id && area_id.length > 0 ? area_id.join(',') : undefined, - supplier_id: - supplier_id && supplier_id.length > 0 - ? supplier_id.join(',') - : undefined, - product_id: - product_id && product_id.length > 0 - ? product_id.join(',') - : undefined, - product_category_id: - product_category_id && product_category_id.length > 0 - ? product_category_id.join(',') - : undefined, + area_id: area_id, + supplier_id: supplier_id, + product_id: product_id, + product_category_id: product_category_id, received_date: received_date, po_date: po_date, start_date: start_date, From 85fddcb19acb857bee00c9e0339147607bce1712 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 13:39:05 +0700 Subject: [PATCH 48/56] refactor(FE-364): Timestamp purchases-per-supplier export filenames --- .../export/PurchasesPerSupplierExport.tsx | 2 +- .../tab/PurchasesPerSupplierTab.tsx | 27 +------------------ 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index cd4603ba..0c99e62e 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -421,7 +421,7 @@ export const generatePurchasesPerSupplierPDF = async ( const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = `laporan-pembelian-per-supplier-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`; + link.download = `laporan-pembelian-per-supplier-dicetak-pada${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`; document.body.appendChild(link); link.click(); document.body.removeChild(link); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 058cd049..fa1790e5 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -361,31 +361,6 @@ const PurchasesPerSupplierTab = () => { const workbook = XLSX.utils.book_new(); - const areaName = - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id - .map( - (id) => - areaOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Area' - : 'Semua Area'; - - const supplierName = - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id - .map( - (id) => - supplierOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Supplier' - : 'Semua Supplier'; - - const startDate = tableFilterState.start_date || 'all'; - const endDate = tableFilterState.end_date || 'all'; - Object.entries(groupedBySupplier).forEach( ([supplierName, supplierData]) => { const totals = supplierData.reduce( @@ -477,7 +452,7 @@ const PurchasesPerSupplierTab = () => { } ); - const filename = `Laporan_Pembelian_Per_Supplier_${areaName}_${supplierName}_${startDate}_to_${endDate}.xlsx`; + const filename = `laporan-pembelian-per-supplier-dicetak-pada${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; XLSX.writeFile(workbook, filename); toast.success('Excel berhasil dibuat dan diunduh.'); From 8fb1ccbdceb8bf6f20a9298a32f8cd0bf9bcb523 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 13:40:26 +0700 Subject: [PATCH 49/56] refactor(FE-364): Add hyphen before timestamp in export filenames --- .../report/logistic-stock/export/PurchasesPerSupplierExport.tsx | 2 +- .../pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index 0c99e62e..ec46bd9f 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -421,7 +421,7 @@ export const generatePurchasesPerSupplierPDF = async ( const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = `laporan-pembelian-per-supplier-dicetak-pada${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`; + link.download = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`; document.body.appendChild(link); link.click(); document.body.removeChild(link); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index fa1790e5..e345ad58 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -452,7 +452,7 @@ const PurchasesPerSupplierTab = () => { } ); - const filename = `laporan-pembelian-per-supplier-dicetak-pada${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; XLSX.writeFile(workbook, filename); toast.success('Excel berhasil dibuat dan diunduh.'); From 1be596921a03b9e1092925a936b9a07c8feb9112 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 13:50:38 +0700 Subject: [PATCH 50/56] refactor(FE-363): Fetch export data on demand via callback --- .../tab/PurchasesPerSupplierTab.tsx | 138 ++++++++++-------- 1 file changed, 74 insertions(+), 64 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index e345ad58..881c85f7 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -275,47 +275,43 @@ const PurchasesPerSupplierTab = () => { ? purchasePerSupplier.meta : null; - const { data: allDataForExport } = useSWR( - isSubmitted - ? () => { - const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - supplier_id: - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id.join(',') - : undefined, - product_id: - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id.join(',') - : undefined, - product_category_id: - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id.join(',') - : undefined, - received_date: - tableFilterState.filter_by === 'received_date' - ? tableFilterState.start_date || undefined - : undefined, - po_date: - tableFilterState.filter_by === 'po_date' - ? tableFilterState.start_date || undefined - : undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - sort_by: tableFilterState.sort_by || undefined, - filter_by: tableFilterState.filter_by || undefined, - limit: 10000, - page: 1, - }; + // ===== EXPORT DATA FETCHER ===== + const logisticPurchasePerSupplierExport = + useCallback(async (): Promise => { + const params = { + area_id: + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id.join(',') + : undefined, + supplier_id: + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id.join(',') + : undefined, + product_id: + tableFilterState.product_id.length > 0 + ? tableFilterState.product_id.join(',') + : undefined, + product_category_id: + tableFilterState.product_category_id.length > 0 + ? tableFilterState.product_category_id.join(',') + : undefined, + received_date: + tableFilterState.filter_by === 'received_date' + ? tableFilterState.start_date || undefined + : undefined, + po_date: + tableFilterState.filter_by === 'po_date' + ? tableFilterState.start_date || undefined + : undefined, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + sort_by: tableFilterState.sort_by || undefined, + filter_by: tableFilterState.filter_by || undefined, + limit: 10000, + page: 1, + }; - return ['logistic-purchase-report-export', params]; - } - : null, - ([, params]) => - LogisticApi.getLogisticPurchasePerSupplierReport( + const response = await LogisticApi.getLogisticPurchasePerSupplierReport( params.area_id, params.supplier_id, params.product_id, @@ -328,27 +324,28 @@ const PurchasesPerSupplierTab = () => { params.filter_by, params.page, params.limit - ) - ); + ); - const allExportData: LogisticPurchasePerSupplierReport['rows'] = useMemo( - () => - isResponseSuccess(allDataForExport) - ? (allDataForExport?.data - ?.rows as LogisticPurchasePerSupplierReport['rows']) || [] - : [], - [allDataForExport] - ); + return isResponseSuccess(response) ? response.data : null; + }, [tableFilterState]); // ===== EXPORT HANDLERS ===== - const handleExportExcel = useCallback(() => { - if (allExportData.length === 0) { - toast.error('Tidak ada data untuk diekspor.'); - return; - } + const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); - try { + const allDataForExport = await logisticPurchasePerSupplierExport(); + + if ( + !allDataForExport || + !allDataForExport?.rows || + allDataForExport.rows.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const allExportData = + allDataForExport.rows as LogisticPurchasePerSupplierReport['rows']; const groupedBySupplier: { [key: string]: typeof allExportData } = {}; allExportData.forEach((item) => { @@ -461,16 +458,26 @@ const PurchasesPerSupplierTab = () => { } finally { setIsExcelExportLoading(false); } - }, [allExportData, tableFilterState, areaOptions, supplierOptions]); + }, [ + logisticPurchasePerSupplierExport, + tableFilterState, + areaOptions, + supplierOptions, + ]); const handleExportPdf = useCallback(async () => { - if (allExportData.length === 0) { - toast.error('Tidak ada data untuk diekspor.'); - return; - } - setIsPdfExportLoading(true); try { + const allDataForExport = await logisticPurchasePerSupplierExport(); + + if ( + !allDataForExport || + !allDataForExport?.rows || + allDataForExport.rows.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } const areaName = tableFilterState.area_id.length > 0 ? tableFilterState.area_id @@ -526,7 +533,10 @@ const PurchasesPerSupplierTab = () => { end_date: tableFilterState.end_date || '', }; - await generatePurchasesPerSupplierPDF(allExportData, exportParams); + await generatePurchasesPerSupplierPDF( + allDataForExport.rows, + exportParams + ); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); @@ -534,7 +544,7 @@ const PurchasesPerSupplierTab = () => { setIsPdfExportLoading(false); } }, [ - allExportData, + logisticPurchasePerSupplierExport, tableFilterState, areaOptions, supplierOptions, From 81d242bd1d9cfd30c6dee4f03ffc4fb42c2c4fa3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 13:57:58 +0700 Subject: [PATCH 51/56] refactor(FE-361,363): Add sort order selector to PurchasesPerSupplierTab --- .../tab/PurchasesPerSupplierTab.tsx | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 881c85f7..90018e98 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -102,6 +102,14 @@ const PurchasesPerSupplierTab = () => { [] ); + const sortByOptions = useMemo( + () => [ + { value: 'ASC', label: 'Ascending' }, + { value: 'DESC', label: 'Descending' }, + ], + [] + ); + const areaChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const arr = Array.isArray(val) ? val : val ? [val] : []; @@ -163,6 +171,16 @@ const PurchasesPerSupplierTab = () => { [updateFilter] ); + const sortByHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + const sortValue = (newVal?.value as 'ASC' | 'DESC') || 'ASC'; + updateFilter('sort_by', sortValue); + setIsSubmitted(false); + }, + [updateFilter] + ); + const startDateChangeHandler = useCallback< ChangeEventHandler >( @@ -843,19 +861,34 @@ const PurchasesPerSupplierTab = () => { isLoading={isLoadingProductCategories} isClearable /> - option.value === tableFilterState.filter_by - ) || null - } - onChange={dataTypeChangeHandler} - isLoading={false} - isClearable={false} - /> +
+ option.value === tableFilterState.filter_by + ) || null + } + onChange={dataTypeChangeHandler} + isLoading={false} + isClearable={false} + /> + option.value === tableFilterState.sort_by + ) || null + } + onChange={sortByHandler} + isLoading={false} + isClearable={false} + /> +
Date: Thu, 18 Dec 2025 13:59:11 +0700 Subject: [PATCH 52/56] refactor(FE-363): Use relative endpoint for logistic reports --- src/services/api/report/logistic-stock.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/services/api/report/logistic-stock.ts b/src/services/api/report/logistic-stock.ts index 753e0aaf..872166ed 100644 --- a/src/services/api/report/logistic-stock.ts +++ b/src/services/api/report/logistic-stock.ts @@ -47,7 +47,8 @@ export class LogisticApiService extends BaseApiService< } } -// TODO: REPLACE WITH PRODUCTION URL -export const LogisticApi = new LogisticApiService( - 'http://localhost:4010/api/reports/logistics' -); +export const LogisticApi = new LogisticApiService('reports/logistics'); + +// export const LogisticApi = new LogisticApiService( +// 'http://localhost:4010/api/reports/logistics' +// ); From 2d8e479b6c72efd4eda122a1ed8c814b44340a34 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 14:16:00 +0700 Subject: [PATCH 53/56] refactor(FE-363): Switch LogisticApi service to reports endpoint --- src/services/api/report/logistic-stock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/api/report/logistic-stock.ts b/src/services/api/report/logistic-stock.ts index 872166ed..13ac5032 100644 --- a/src/services/api/report/logistic-stock.ts +++ b/src/services/api/report/logistic-stock.ts @@ -47,7 +47,7 @@ export class LogisticApiService extends BaseApiService< } } -export const LogisticApi = new LogisticApiService('reports/logistics'); +export const LogisticApi = new LogisticApiService('reports'); // export const LogisticApi = new LogisticApiService( // 'http://localhost:4010/api/reports/logistics' From 20494657c6cfca2eaea8de6dc97a1327600c15b5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 14:35:10 +0700 Subject: [PATCH 54/56] refactor(FE-363): Extract row and summary types for logistic report --- src/types/api/report/logistic-stock.d.ts | 44 +++++++++++++++--------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts index 77d0b5d3..0d5e60c3 100644 --- a/src/types/api/report/logistic-stock.d.ts +++ b/src/types/api/report/logistic-stock.d.ts @@ -3,21 +3,31 @@ import { Supplier } from '@/types/api/supplier/supplier'; import { Product } from '@/types/api/product/product'; import { Warehouse } from '@/types/api/warehouse/warehouse'; -export type LogisticPurchasePerSupplierReport = BaseMetadata & { - rows: { - supplier: Supplier; - receive_date: string; - po_date: string; - po_number: string; - product: Product; - warehouse: Warehouse; - qty: number; - unit_price: number; - purchase_value: number; - transport_unit_price: number; - transport_value: number; - total_amount: number; - expedition: string; - delivery_number: string; - }[]; +export type LogisticPurchasePerSupplierReportRow = { + receive_date: string; + po_date: string; + po_number: string; + product: Product; + warehouse: Warehouse; + qty: number; + unit_price: number; + purchase_value: number; + transport_unit_price: number; + transport_value: number; + total_amount: number; + expedition: string; + delivery_number: string; +}; + +export type LogisticPurchasePerSupplierSummary = { + total_qty: number; + total_purchase_value: number; + total_transport_value: number; + total_amount: number; +}; + +export type LogisticPurchasePerSupplierReport = BaseMetadata & { + supplier: Supplier; + rows: LogisticPurchasePerSupplierReportRow[]; + summary: LogisticPurchasePerSupplierSummary; }; From d001b05c4e020ee91f4188e5dba4b0da2ac74303 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 14:58:54 +0700 Subject: [PATCH 55/56] refactor(FE-361,363,364): Use per-supplier report arrays for export --- .../tab/PurchasesPerSupplierTab.tsx | 381 +++++++----------- src/types/api/report/logistic-stock.d.ts | 2 + 2 files changed, 154 insertions(+), 229 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 90018e98..81913748 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -15,7 +15,10 @@ import { LogisticApi } from '@/services/api/report/logistic-stock'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; -import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; +import { + LogisticPurchasePerSupplierReport, + LogisticPurchasePerSupplierSummary, +} from '@/types/api/report/logistic-stock'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import Pagination from '@/components/Pagination'; @@ -26,22 +29,6 @@ import Menu from '@/components/menu/Menu'; import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport'; import toast from 'react-hot-toast'; import * as XLSX from 'xlsx'; -import { Supplier } from '@/types/api/master-data/supplier'; - -interface Totals { - total_qty: number; - total_price: number; - total_purchase_amount: number; - total_transport: number; - total_value_transport: number; - total_jumlah: number; -} - -interface GroupedSupplierData { - id: number; - supplier: Supplier; - items: LogisticPurchasePerSupplierReport['rows']; -} const PurchasesPerSupplierTab = () => { // ===== STATE MANAGEMENT ===== @@ -279,11 +266,11 @@ const PurchasesPerSupplierTab = () => { ) ); - const data: LogisticPurchasePerSupplierReport['rows'] = useMemo( + const data: LogisticPurchasePerSupplierReport[] = useMemo( () => isResponseSuccess(purchasePerSupplier) - ? (purchasePerSupplier?.data - ?.rows as LogisticPurchasePerSupplierReport['rows']) || [] + ? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplierReport[]) || + [] : [], [purchasePerSupplier] ); @@ -294,58 +281,61 @@ const PurchasesPerSupplierTab = () => { : null; // ===== EXPORT DATA FETCHER ===== - const logisticPurchasePerSupplierExport = - useCallback(async (): Promise => { - const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - supplier_id: - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id.join(',') - : undefined, - product_id: - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id.join(',') - : undefined, - product_category_id: - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id.join(',') - : undefined, - received_date: - tableFilterState.filter_by === 'received_date' - ? tableFilterState.start_date || undefined - : undefined, - po_date: - tableFilterState.filter_by === 'po_date' - ? tableFilterState.start_date || undefined - : undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - sort_by: tableFilterState.sort_by || undefined, - filter_by: tableFilterState.filter_by || undefined, - limit: 10000, - page: 1, - }; + const logisticPurchasePerSupplierExport = useCallback(async (): Promise< + LogisticPurchasePerSupplierReport[] | null + > => { + const params = { + area_id: + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id.join(',') + : undefined, + supplier_id: + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id.join(',') + : undefined, + product_id: + tableFilterState.product_id.length > 0 + ? tableFilterState.product_id.join(',') + : undefined, + product_category_id: + tableFilterState.product_category_id.length > 0 + ? tableFilterState.product_category_id.join(',') + : undefined, + received_date: + tableFilterState.filter_by === 'received_date' + ? tableFilterState.start_date || undefined + : undefined, + po_date: + tableFilterState.filter_by === 'po_date' + ? tableFilterState.start_date || undefined + : undefined, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + sort_by: tableFilterState.sort_by || undefined, + filter_by: tableFilterState.filter_by || undefined, + limit: 10000, + page: 1, + }; - const response = await LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_id, - params.supplier_id, - params.product_id, - params.product_category_id, - params.received_date, - params.po_date, - params.start_date, - params.end_date, - params.sort_by, - params.filter_by, - params.page, - params.limit - ); + const response = await LogisticApi.getLogisticPurchasePerSupplierReport( + params.area_id, + params.supplier_id, + params.product_id, + params.product_category_id, + params.received_date, + params.po_date, + params.start_date, + params.end_date, + params.sort_by, + params.filter_by, + params.page, + params.limit + ); - return isResponseSuccess(response) ? response.data : null; - }, [tableFilterState]); + return isResponseSuccess(response) + ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) + : null; + }, [tableFilterState]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -355,73 +345,43 @@ const PurchasesPerSupplierTab = () => { if ( !allDataForExport || - !allDataForExport?.rows || - allDataForExport.rows.length === 0 + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); return; } - const allExportData = - allDataForExport.rows as LogisticPurchasePerSupplierReport['rows']; - const groupedBySupplier: { [key: string]: typeof allExportData } = {}; - - allExportData.forEach((item) => { - const supplierName = item.supplier?.name || 'Unknown Supplier'; - if (!groupedBySupplier[supplierName]) { - groupedBySupplier[supplierName] = []; - } - groupedBySupplier[supplierName].push(item); - }); - const workbook = XLSX.utils.book_new(); - Object.entries(groupedBySupplier).forEach( - ([supplierName, supplierData]) => { - const totals = supplierData.reduce( - (acc, item) => ({ - total_qty: acc.total_qty + (item.qty || 0), - total_price: acc.total_price + (item.unit_price || 0), - total_purchase_amount: - acc.total_purchase_amount + (item.purchase_value || 0), - total_transport: - acc.total_transport + (item.transport_unit_price || 0), - total_value_transport: - acc.total_value_transport + (item.transport_value || 0), - total_jumlah: acc.total_jumlah + (item.total_amount || 0), - }), - { - total_qty: 0, - total_price: 0, - total_purchase_amount: 0, - total_transport: 0, - total_value_transport: 0, - total_jumlah: 0, - } - ); + allDataForExport.forEach((supplierReport) => { + const supplierData = supplierReport.rows; + const supplierName = + supplierReport.supplier?.name || 'Unknown Supplier'; - const excelData: { [key: string]: string | number }[] = - supplierData.map((item, index) => ({ - No: index + 1, - 'Tanggal Terima': item.receive_date - ? formatDate(item.receive_date, 'DD MMM YYYY') - : '', - 'Tanggal PO': item.po_date - ? formatDate(item.po_date, 'DD MMM YYYY') - : '', - 'No. Referensi': item.po_number || '', - 'Nama Produk': item.product?.name || '', - Tujuan: item.warehouse?.name || '', - QTY: item.qty || 0, - 'Harga Beli (Rp)': item.unit_price || 0, - 'Value Harga Beli (Rp)': item.purchase_value || 0, - 'Transport (Rp)': item.transport_unit_price || 0, - 'Value Transport (Rp)': item.transport_value || 0, - 'Jumlah (Rp)': item.total_amount || 0, - Ekspedisi: item.expedition || '', - 'Surat Jalan': item.delivery_number || '', - })); + const excelData: { [key: string]: string | number }[] = + supplierData.map((item, index) => ({ + No: index + 1, + 'Tanggal Terima': item.receive_date + ? formatDate(item.receive_date, 'DD MMM YYYY') + : '', + 'Tanggal PO': item.po_date + ? formatDate(item.po_date, 'DD MMM YYYY') + : '', + 'No. Referensi': item.po_number || '', + 'Nama Produk': item.product?.name || '', + Tujuan: item.warehouse?.name || '', + QTY: item.qty || 0, + 'Harga Beli (Rp)': item.unit_price || 0, + 'Value Harga Beli (Rp)': item.purchase_value || 0, + 'Transport (Rp)': item.transport_unit_price || 0, + 'Value Transport (Rp)': item.transport_value || 0, + 'Jumlah (Rp)': item.total_amount || 0, + Ekspedisi: item.expedition || '', + 'Surat Jalan': item.delivery_number || '', + })); + if (supplierReport.summary) { excelData.push({ No: 'Total', 'Tanggal Terima': '', @@ -429,43 +389,45 @@ const PurchasesPerSupplierTab = () => { 'No. Referensi': '', 'Nama Produk': '', Tujuan: '', - QTY: totals.total_qty, - 'Harga Beli (Rp)': totals.total_price, - 'Value Harga Beli (Rp)': totals.total_purchase_amount, - 'Transport (Rp)': totals.total_transport, - 'Value Transport (Rp)': totals.total_value_transport, - 'Jumlah (Rp)': totals.total_jumlah, + QTY: supplierReport.summary.total_qty || 0, + 'Harga Beli (Rp)': '', + 'Value Harga Beli (Rp)': + supplierReport.summary.total_purchase_value || 0, + 'Transport (Rp)': '', + 'Value Transport (Rp)': + supplierReport.summary.total_transport_value || 0, + 'Jumlah (Rp)': supplierReport.summary.total_amount || 0, Ekspedisi: '', 'Surat Jalan': '', }); - - const worksheet = XLSX.utils.json_to_sheet(excelData); - - const colWidths = [ - { wch: 5 }, // No - { wch: 15 }, // Tanggal Terima - { wch: 15 }, // Tanggal PO - { wch: 15 }, // No. Referensi - { wch: 30 }, // Nama Produk - { wch: 20 }, // Tujuan - { wch: 10 }, // QTY - { wch: 18 }, // Harga Beli - { wch: 20 }, // Value Harga Beli - { wch: 15 }, // Transport - { wch: 20 }, // Value Transport - { wch: 18 }, // Jumlah - { wch: 15 }, // Ekspedisi - { wch: 15 }, // Surat Jalan - ]; - worksheet['!cols'] = colWidths; - - const sheetName = - supplierName.length > 31 - ? supplierName.substring(0, 31) - : supplierName; - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); } - ); + + const worksheet = XLSX.utils.json_to_sheet(excelData); + + const colWidths = [ + { wch: 5 }, // No + { wch: 15 }, // Tanggal Terima + { wch: 15 }, // Tanggal PO + { wch: 15 }, // No. Referensi + { wch: 30 }, // Nama Produk + { wch: 20 }, // Tujuan + { wch: 10 }, // QTY + { wch: 18 }, // Harga Beli + { wch: 20 }, // Value Harga Beli + { wch: 15 }, // Transport + { wch: 20 }, // Value Transport + { wch: 18 }, // Jumlah + { wch: 15 }, // Ekspedisi + { wch: 15 }, // Surat Jalan + ]; + worksheet['!cols'] = colWidths; + + const sheetName = + supplierName.length > 31 + ? supplierName.substring(0, 31) + : supplierName; + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; @@ -490,12 +452,17 @@ const PurchasesPerSupplierTab = () => { if ( !allDataForExport || - !allDataForExport?.rows || - allDataForExport.rows.length === 0 + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); return; } + + const allRows = allDataForExport.flatMap( + (supplierReport) => supplierReport.rows + ); + const areaName = tableFilterState.area_id.length > 0 ? tableFilterState.area_id @@ -551,10 +518,7 @@ const PurchasesPerSupplierTab = () => { end_date: tableFilterState.end_date || '', }; - await generatePurchasesPerSupplierPDF( - allDataForExport.rows, - exportParams - ); + await generatePurchasesPerSupplierPDF(allRows, exportParams); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); @@ -591,28 +555,8 @@ const PurchasesPerSupplierTab = () => { } }; - // ===== TABLE COLUMNS DEFINITION ===== - const groupedData = useMemo(() => { - const groups: { [key: number]: GroupedSupplierData } = {}; - - data.forEach((item) => { - const supplierId = item.supplier?.id; - if (supplierId && !groups[supplierId]) { - groups[supplierId] = { - id: supplierId, - supplier: item.supplier, - items: [], - }; - } - if (groups[supplierId]) { - groups[supplierId].items.push(item); - } - }); - - return Object.values(groups); - }, [data]); const getTableColumns = ( - totals: Totals + summary: LogisticPurchasePerSupplierSummary ): ColumnDef[] => { const tableColumns: ColumnDef< LogisticPurchasePerSupplierReport['rows'][0] @@ -679,7 +623,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatNumber(totals.total_qty)} + {formatNumber(summary.total_qty) || '-'}
), }, @@ -693,7 +637,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_price)} + {formatCurrency(summary.total_unit_price) || '-'}
), }, @@ -707,7 +651,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_purchase_amount)} + {formatCurrency(summary.total_purchase_value) || '-'}
), }, @@ -721,7 +665,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_transport)} + {formatCurrency(summary.total_transport_unit_price) || '-'}
), }, @@ -735,7 +679,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_value_transport)} + {formatCurrency(summary.total_transport_value) || '-'}
), }, @@ -749,7 +693,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_jumlah)} + {formatCurrency(summary.total_amount) || '-'}
), }, @@ -920,54 +864,33 @@ const PurchasesPerSupplierTab = () => { Tidak ada data yang dapat ditampilkan...
) : ( - groupedData.map((supplier) => { - const total_qty = supplier.items.reduce( - (sum, item) => sum + (item.qty || 0), - 0 - ); - const total_price = supplier.items.reduce( - (sum, item) => sum + (item.purchase_value || 0), - 0 - ); - const total_transport = supplier.items.reduce( - (sum, item) => sum + (item.transport_value || 0), - 0 - ); - const total_value_transport = supplier.items.reduce( - (sum, item) => sum + (item.transport_value || 0), - 0 - ); - const total_jumlah = supplier.items.reduce( - (sum, item) => sum + (item.total_amount || 0), - 0 - ); - - const totals = { - total_qty, - total_price, - total_purchase_amount: total_price, - total_transport, - total_value_transport, - total_jumlah, + data.map((supplierReport) => { + const summary = supplierReport.summary || { + total_qty: 0, + total_unit_price: 0, + total_purchase_value: 0, + total_transport_unit_price: 0, + total_transport_value: 0, + total_amount: 0, }; - const totalPurchase = totals.total_jumlah; - const tableColumns = getTableColumns(totals); + const totalPurchase = summary.total_amount; + const tableColumns = getTableColumns(summary); return (
0} + renderFooter={supplierReport.rows.length > 0} className={{ containerClassName: 'w-full', tableWrapperClassName: 'overflow-x-auto mt-4', diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts index 0d5e60c3..e5f0f2c6 100644 --- a/src/types/api/report/logistic-stock.d.ts +++ b/src/types/api/report/logistic-stock.d.ts @@ -21,7 +21,9 @@ export type LogisticPurchasePerSupplierReportRow = { export type LogisticPurchasePerSupplierSummary = { total_qty: number; + total_unit_price: number; total_purchase_value: number; + total_transport_unit_price: number; total_transport_value: number; total_amount: number; }; From 36389bae2a87248d1085a8fc0ecd0763e3c98447 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 15:11:34 +0700 Subject: [PATCH 56/56] refactor(FE-364): Refactor purchases export to use supplier reports --- .../export/PurchasesPerSupplierExport.tsx | 66 ++++++------------- .../tab/PurchasesPerSupplierTab.tsx | 10 +-- 2 files changed, 22 insertions(+), 54 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index ec46bd9f..a7967159 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -10,7 +10,7 @@ import { pdf, } from '@react-pdf/renderer'; import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; -import { formatDate, formatNumber } from '@/lib/helper'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; Font.register({ family: 'Helvetica', @@ -177,7 +177,7 @@ const pdfStyles = StyleSheet.create({ }); interface PurchasesPerSupplierExportParams { - data: LogisticPurchasePerSupplierReport['rows']; + data: LogisticPurchasePerSupplierReport[]; params: { area_name?: string; supplier_name?: string; @@ -192,36 +192,6 @@ interface PurchasesPerSupplierExportParams { }; } -interface GroupedSupplierData { - id: number; - supplier: LogisticPurchasePerSupplierReport['rows'][number]['supplier']; - items: LogisticPurchasePerSupplierReport['rows'][number][]; -} - -const groupDataBySupplier = ( - data: LogisticPurchasePerSupplierReport['rows'] -): GroupedSupplierData[] => { - const groups: { - [key: number]: GroupedSupplierData; - } = {}; - - data.forEach((item) => { - const supplierId = item.supplier?.id; - if (supplierId && !groups[supplierId]) { - groups[supplierId] = { - id: supplierId, - supplier: item.supplier, - items: [], - }; - } - if (groups[supplierId]) { - groups[supplierId].items.push(item); - } - }); - - return Object.values(groups) as GroupedSupplierData[]; -}; - const getParameterText = ( params: PurchasesPerSupplierExportParams['params'] ) => { @@ -249,7 +219,7 @@ const getParameterText = ( }; const createPDFDocument = ( - groupedData: GroupedSupplierData[], + supplierReports: LogisticPurchasePerSupplierReport[], params: PurchasesPerSupplierExportParams['params'] ) => ( @@ -277,20 +247,23 @@ const createPDFDocument = ( {/* Supplier Sections */} - {groupedData.map( - (supplierGroup: GroupedSupplierData, supplierIndex: number) => { + {supplierReports.map( + ( + supplierReport: LogisticPurchasePerSupplierReport, + supplierIndex: number + ) => { return ( - {supplierGroup.supplier.name} + {supplierReport.supplier.name} @@ -338,7 +311,7 @@ const createPDFDocument = ( {/* Table Body */} - {supplierGroup.items.map( + {supplierReport.rows.map( ( item: LogisticPurchasePerSupplierReport['rows'][number], index: number @@ -347,7 +320,7 @@ const createPDFDocument = ( key={index} style={[ pdfStyles.tableRow, - index < supplierGroup.items.length - 1 + index < supplierReport.rows.length - 1 ? pdfStyles.tableBorderBottom : {}, ]} @@ -376,18 +349,18 @@ const createPDFDocument = ( {formatNumber(item.qty || 0)} - {formatNumber(item.unit_price || 0)} + {formatCurrency(item.unit_price || 0)} - {formatNumber(item.purchase_value || 0)} + {formatCurrency(item.purchase_value || 0)} - {formatNumber(item.transport_unit_price || 0)} + {formatCurrency(item.transport_unit_price || 0)} - {formatNumber(item.total_amount || 0)} + {formatCurrency(item.total_amount || 0)} @@ -410,11 +383,10 @@ const createPDFDocument = ( ); export const generatePurchasesPerSupplierPDF = async ( - data: LogisticPurchasePerSupplierReport['rows'], + data: LogisticPurchasePerSupplierReport[], params: PurchasesPerSupplierExportParams['params'] ): Promise => { - const groupedData = groupDataBySupplier(data); - const PDFDocument = createPDFDocument(groupedData, params); + const PDFDocument = createPDFDocument(data, params); try { const blob = await pdf(PDFDocument).toBlob(); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 81913748..dac2d02e 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -313,7 +313,7 @@ const PurchasesPerSupplierTab = () => { end_date: tableFilterState.end_date || undefined, sort_by: tableFilterState.sort_by || undefined, filter_by: tableFilterState.filter_by || undefined, - limit: 10000, + limit: 100, page: 1, }; @@ -459,10 +459,6 @@ const PurchasesPerSupplierTab = () => { return; } - const allRows = allDataForExport.flatMap( - (supplierReport) => supplierReport.rows - ); - const areaName = tableFilterState.area_id.length > 0 ? tableFilterState.area_id @@ -518,7 +514,7 @@ const PurchasesPerSupplierTab = () => { end_date: tableFilterState.end_date || '', }; - await generatePurchasesPerSupplierPDF(allRows, exportParams); + await generatePurchasesPerSupplierPDF(allDataForExport, exportParams); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); @@ -821,7 +817,7 @@ const PurchasesPerSupplierTab = () => { />