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'
>