diff --git a/package-lock.json b/package-lock.json index f0212474..0c0c75ed 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", @@ -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" }, @@ -1082,9 +1083,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 +1856,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 +1926,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 +2450,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3060,7 +3064,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 +3521,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 +3695,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 +5661,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 +6174,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 +6205,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 +7092,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7250,6 +7260,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7525,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 52fc6ce2..d0b99b80 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", @@ -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" }, 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..77ba31ed --- /dev/null +++ b/src/app/report/logistic-stock/page.tsx @@ -0,0 +1,7 @@ +import LogisticStockTabs from '@/components/pages/report/logistic-stock/LogisticStockTabs'; + +const LogisticStock = () => { + return ; +}; + +export default LogisticStock; 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; 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..1e2d2824 --- /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/tab/PurchasesPerSupplierTab'; + +const LogisticStockTabs = () => { + const tabs = [ + { + id: '1', + 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', + // }, + ]; + + return ( +
+ +
+ ); +}; + +export default LogisticStockTabs; 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..a7967159 --- /dev/null +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -0,0 +1,404 @@ +'use client'; + +import { + Page, + Text, + View, + Document, + StyleSheet, + Font, + pdf, +} from '@react-pdf/renderer'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; + +Font.register({ + family: 'Helvetica', + src: 'helvetica', +}); + +const pdfStyles = StyleSheet.create({ + page: { + fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', + }, + titleSection: { + marginBottom: 10, + }, + mainTitle: { + fontSize: 14, + fontWeight: 'bold', + marginBottom: 5, + color: '#1f74bf', + }, + 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, + textAlign: 'left', + }, + tableCellNo: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + textAlign: 'center', + }, + tableCellLast: { + flex: 1, + padding: 4, + fontSize: 8, + }, + tableCellHeader: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + textAlign: 'center', + }, + tableCellHeaderRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + textAlign: 'right', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + }, + tableCellHeaderLast: { + flex: 1, + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + 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', + }, + tableCellCenterLast: { + flex: 1, + padding: 4, + fontSize: 8, + textAlign: 'center', + }, + tableBorderBottom: { + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + }, + supplierSection: { + marginBottom: 10, + }, + supplierSectionBreak: { + marginBottom: 15, + }, + badge: { + backgroundColor: '#1f74bf', + color: '#FFFFFF', + padding: 2, + borderRadius: 2, + fontSize: 7, + fontWeight: 'bold', + alignSelf: 'center', + marginRight: 4, + }, + parameterBadge: { + backgroundColor: '#F5F5F5', + color: '#333333', + padding: 4, + borderRadius: 4, + fontSize: 8, + marginRight: 8, + marginBottom: 4, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, +}); + +interface PurchasesPerSupplierExportParams { + data: LogisticPurchasePerSupplierReport[]; + 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; + }; +} + +const getParameterText = ( + params: PurchasesPerSupplierExportParams['params'] +) => { + const paramsText = []; + + 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; +}; + +const createPDFDocument = ( + supplierReports: LogisticPurchasePerSupplierReport[], + params: PurchasesPerSupplierExportParams['params'] +) => ( + + + {/* Title and Parameters */} + + + Laporan > Rekapitulasi Pembelian Per Supplier + + + + + Jenis Tanggal:{' '} + {params.filter_by === 'received_date' + ? 'Tanggal Terima' + : 'Tanggal PO'} + + + {getParameterText(params).map((param, index) => ( + + {param} + + ))} + + + + {/* Supplier Sections */} + {supplierReports.map( + ( + supplierReport: LogisticPurchasePerSupplierReport, + supplierIndex: number + ) => { + return ( + + + {supplierReport.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 */} + {supplierReport.rows.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)} + + + {formatCurrency(item.unit_price || 0)} + + + {formatCurrency(item.purchase_value || 0)} + + + + {formatCurrency(item.transport_unit_price || 0)} + + + + {formatCurrency(item.total_amount || 0)} + + + + {item.expedition || '-'} + + + + {item.delivery_number || '-'} + + + ) + )} + + + ); + } + )} + + +); + +export const generatePurchasesPerSupplierPDF = async ( + data: LogisticPurchasePerSupplierReport[], + params: PurchasesPerSupplierExportParams['params'] +): Promise => { + const PDFDocument = createPDFDocument(data, 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-dicetak-pada-${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 new file mode 100644 index 00000000..dac2d02e --- /dev/null +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -0,0 +1,932 @@ +import { useState, useMemo, useCallback } from 'react'; +import { ChangeEventHandler } from 'react'; +import useSWR from 'swr'; +import Card from '@/components/Card'; +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 { ProductCategoryApi } from '@/services/api/master-data'; +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, + LogisticPurchasePerSupplierSummary, +} from '@/types/api/report/logistic-stock'; +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'; +import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport'; +import toast from 'react-hot-toast'; +import * as XLSX from 'xlsx'; + +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); + const [pageSize, setPageSize] = useState(10); + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + + // ===== TABLE FILTER STATE ===== + const { state: tableFilterState, updateFilter } = useTableFilter({ + initial: { + area_id: [] as string[], + supplier_id: [] as string[], + product_id: [] as string[], + product_category_id: [] as string[], + received_date: '', + po_date: '', + start_date: '', + end_date: '', + sort_by: '', + filter_by: 'received_date', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, + }); + + 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 { + options: productCategoryOptions, + isLoadingOptions: isLoadingProductCategories, + } = useSelect(ProductCategoryApi.basePath, 'id', 'name', 'search'); + + const dataTypeOptions = useMemo( + () => [ + { value: 'received_date', label: 'Tanggal Terima' }, + { value: 'po_date', label: 'Tanggal PO' }, + ], + [] + ); + + 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] : []; + updateFilter( + 'area_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const supplierChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'supplier_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const productChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'product_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const productCategoryChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'product_category_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [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 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 + >( + (e) => { + const val = e.target.value; + updateFilter('start_date', val || ''); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const endDateChangeHandler = useCallback< + ChangeEventHandler + >( + (e) => { + const val = e.target.value; + updateFilter('end_date', val || ''); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const resetFilters = useCallback(() => { + 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', ''); + updateFilter('sort_by', ''); + updateFilter('filter_by', 'received_date'); + setIsSubmitted(false); + }, [updateFilter]); + + const handleSubmit = useCallback(() => { + setIsSubmitted(true); + setCurrentPage(1); + }, []); + + // ===== DATA FETCHING ===== + const { data: purchasePerSupplier, isLoading } = 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, + page: currentPage, + limit: pageSize, + }; + + return ['logistic-purchase-report', params]; + } + : null, + ([, params]) => + 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 data: LogisticPurchasePerSupplierReport[] = useMemo( + () => + isResponseSuccess(purchasePerSupplier) + ? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplierReport[]) || + [] + : [], + [purchasePerSupplier] + ); + + const meta = + isResponseSuccess(purchasePerSupplier) && purchasePerSupplier?.meta + ? purchasePerSupplier.meta + : null; + + // ===== EXPORT DATA FETCHER ===== + 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: 100, + 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 + ); + + return isResponseSuccess(response) + ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) + : null; + }, [tableFilterState]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await logisticPurchasePerSupplierExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const workbook = XLSX.utils.book_new(); + + 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 || '', + })); + + if (supplierReport.summary) { + excelData.push({ + No: 'Total', + 'Tanggal Terima': '', + 'Tanggal PO': '', + 'No. Referensi': '', + 'Nama Produk': '', + Tujuan: '', + 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 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.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [ + logisticPurchasePerSupplierExport, + tableFilterState, + areaOptions, + supplierOptions, + ]); + + const handleExportPdf = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await logisticPurchasePerSupplierExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + 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: 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 || '', + }; + + await generatePurchasesPerSupplierPDF(allDataForExport, exportParams); + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [ + logisticPurchasePerSupplierExport, + tableFilterState, + areaOptions, + supplierOptions, + productOptions, + productCategoryOptions, + ]); + + // ===== 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 = ( + summary: LogisticPurchasePerSupplierSummary + ): 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: 'receive_date', + cell: (props) => { + const value = props.row.original.receive_date; + return formatDate(value, 'DD MMM YYYY'); + }, + }, + { + id: 'po_date', + header: 'Tanggal PO', + accessorKey: 'po_date', + 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) => { + const value = props.row.original.po_number; + return value || '-'; + }, + }, + { + id: 'product_name', + header: 'Nama Produk', + accessorKey: 'product.name', + cell: (props) => { + const product = props.row.original.product; + return product?.name || '-'; + }, + }, + { + id: 'destination_warehouse', + header: 'Tujuan', + accessorKey: 'warehouse.name', + cell: (props) => { + const warehouse = props.row.original.warehouse; + return warehouse?.name || '-'; + }, + }, + { + id: 'qty', + header: 'QTY', + accessorKey: 'qty', + cell: (props) => { + const value = props.row.original.qty; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summary.total_qty) || '-'} +
+ ), + }, + { + id: 'price', + header: 'Harga Beli (Rp)', + accessorKey: 'unit_price', + cell: (props) => { + const value = props.row.original.unit_price; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_unit_price) || '-'} +
+ ), + }, + { + id: 'purchase_amount', + header: 'Value Harga Beli (Rp)', + accessorKey: 'purchase_value', + cell: (props) => { + const value = props.row.original.purchase_value; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_purchase_value) || '-'} +
+ ), + }, + { + id: 'transport', + header: 'Transport (Rp)', + accessorKey: 'transport_unit_price', + cell: (props) => { + const value = props.row.original.transport_unit_price; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_transport_unit_price) || '-'} +
+ ), + }, + { + id: 'value_transport', + header: 'Value Transport (Rp)', + accessorKey: 'transport_value', + cell: (props) => { + const value = props.row.original.transport_value; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_transport_value) || '-'} +
+ ), + }, + { + id: 'total', + header: 'Jumlah (Rp)', + accessorKey: 'total_amount', + cell: (props) => { + const value = props.row.original.total_amount; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_amount) || '-'} +
+ ), + }, + { + id: 'expedition_vendor_name', + header: 'Ekspedisi', + accessorKey: 'expedition', + cell: (props) => { + const value = props.row.original.expedition; + return value || '-'; + }, + }, + { + id: 'travel_number', + header: 'Surat Jalan', + accessorKey: 'delivery_number', + cell: (props) => { + const value = props.row.original.delivery_number; + return value || '-'; + }, + }, + ]; + return tableColumns; + }; + + return ( +
+ +
+ + + + Export + + } + align='end' + > + + + + + +
+
+ + (tableFilterState.area_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={areaChangeHandler} + isLoading={isLoadingAreas} + isClearable + /> + + (tableFilterState.supplier_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={supplierChangeHandler} + isLoading={isLoadingSuppliers} + isClearable + /> + + (tableFilterState.product_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={productChangeHandler} + isLoading={isLoadingProducts} + isClearable + /> +
+
+ + (tableFilterState.product_category_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={productCategoryChangeHandler} + isLoading={isLoadingProductCategories} + isClearable + /> +
+ option.value === tableFilterState.filter_by + ) || null + } + onChange={dataTypeChangeHandler} + isLoading={false} + isClearable={false} + /> + option.value === tableFilterState.sort_by + ) || null + } + onChange={sortByHandler} + isLoading={false} + isClearable={false} + /> +
+
+ + +
+
+ + {!isSubmitted ? ( +
+ Silakan pilih filter dan klik tombol Submit untuk menampilkan data. +
+ ) : isLoading ? ( +
+ +
+ ) : data.length === 0 ? ( +
+ Tidak ada data yang dapat ditampilkan... +
+ ) : ( + 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 = summary.total_amount; + const tableColumns = getTableColumns(summary); + + 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', + paginationClassName: 'hidden', + }} + /> + + ); + }) + )} + + {meta && data.length > 0 && ( +
+ +
+ )} + + ); +}; + +export default PurchasesPerSupplierTab; diff --git a/src/config/constant.ts b/src/config/constant.ts index 96fc8401..196afc18 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -45,6 +45,17 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/closing', icon: 'heroicons-outline:presentation-chart-bar', }, + { + text: 'Laporan', + link: '/report', + icon: 'mdi:chart-box-outline', + submenu: [ + { + text: 'Logistik & Persediaan', + link: '/report/logistic-stock', + }, + ], + }, { text: 'Persediaan', link: '/inventory', diff --git a/src/services/api/report/logistic-stock.ts b/src/services/api/report/logistic-stock.ts new file mode 100644 index 00000000..13ac5032 --- /dev/null +++ b/src/services/api/report/logistic-stock.ts @@ -0,0 +1,54 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; + +export class LogisticApiService extends BaseApiService< + LogisticPurchasePerSupplierReport, + unknown, + unknown +> { + constructor(basePath: string) { + super(basePath); + } + + async getLogisticPurchasePerSupplierReport( + area_id?: string, + supplier_id?: string, + product_id?: string, + product_category_id?: string, + received_date?: string, + po_date?: string, + start_date?: string, + end_date?: string, + sort_by?: string, + filter_by?: string, + page?: number, + limit?: number + ): Promise | undefined> { + return await this.customRequest< + BaseApiResponse + >(`purchase-supplier`, { + method: 'GET', + params: { + 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, + }, + }); + } +} + +export const LogisticApi = new LogisticApiService('reports'); + +// export const LogisticApi = new LogisticApiService( +// '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 new file mode 100644 index 00000000..e5f0f2c6 --- /dev/null +++ b/src/types/api/report/logistic-stock.d.ts @@ -0,0 +1,35 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { Supplier } from '@/types/api/supplier/supplier'; +import { Product } from '@/types/api/product/product'; +import { Warehouse } from '@/types/api/warehouse/warehouse'; + +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_unit_price: number; + total_purchase_value: number; + total_transport_unit_price: number; + total_transport_value: number; + total_amount: number; +}; + +export type LogisticPurchasePerSupplierReport = BaseMetadata & { + supplier: Supplier; + rows: LogisticPurchasePerSupplierReportRow[]; + summary: LogisticPurchasePerSupplierSummary; +};