From c7ffae68d84ca7fa2f0ec85e6c080bc715696694 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 21 Jan 2026 14:27:59 +0700 Subject: [PATCH] fix(FE): adding color to negative value excel and change select UI --- package-lock.json | 24 +- src/components/input/SelectInputRadio.tsx | 70 ++++++ .../finance/export/DebtSupplierExportXLSX.tsx | 231 +++++++++++------- .../report/finance/tab/DebtSupplierTab.tsx | 6 +- 4 files changed, 223 insertions(+), 108 deletions(-) create mode 100644 src/components/input/SelectInputRadio.tsx diff --git a/package-lock.json b/package-lock.json index d108e6bb..c7a7af78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4463,7 +4463,6 @@ "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" } @@ -4474,7 +4473,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4547,7 +4545,6 @@ "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", @@ -5071,7 +5068,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6002,8 +5998,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", @@ -6430,8 +6425,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -6701,7 +6695,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6875,7 +6868,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8508,7 +8500,6 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -9961,7 +9952,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9992,7 +9982,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10060,8 +10049,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-number-format": { "version": "5.4.4", @@ -10078,7 +10066,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -10291,8 +10278,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -11205,7 +11191,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11391,7 +11376,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/components/input/SelectInputRadio.tsx b/src/components/input/SelectInputRadio.tsx new file mode 100644 index 00000000..73608931 --- /dev/null +++ b/src/components/input/SelectInputRadio.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useMemo } from 'react'; +import { + OptionProps, + GroupBase, + components as ReactSelectComponents, +} from 'react-select'; +import SelectInput, { OptionType, SelectInputProps } from './SelectInput'; +import { cn } from '@/lib/helper'; + +interface SelectInputRadioProps + extends Omit, 'closeMenuOnSelect' | 'optionComponent'> { + closeMenuOnSelect?: boolean; +} + +const RadioOption = < + T extends OptionType, + IsMulti extends boolean, + Group extends GroupBase, +>( + props: OptionProps +) => { + const { isSelected, label, innerRef, innerProps, className } = props; + + return ( +
+ null} + className='radio radio-sm radio-primary pointer-events-none' + /> + +
+ ); +}; + +const SelectInputRadio = ( + props: SelectInputRadioProps +) => { + const { closeMenuOnSelect = true, className, ...restProps } = props; + + const customComponents = useMemo(() => { + return { + Option: RadioOption as typeof ReactSelectComponents.Option, + }; + }, []); + + return ( + + {...restProps} + closeMenuOnSelect={closeMenuOnSelect} + className={{ + ...className, + select: cn(className?.select, 'select-radio'), + }} + components={customComponents} + /> + ); +}; + +export default SelectInputRadio; diff --git a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx index 39e0cec4..e5de3ae2 100644 --- a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx +++ b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx @@ -1,6 +1,6 @@ 'use client'; -import * as XLSX from 'xlsx'; +import ExcelJS from 'exceljs'; import { formatDate } from '@/lib/helper'; import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier'; @@ -8,115 +8,174 @@ interface DebtSupplierExportExcelParams { data: DebtSupplier[]; } -export const generateDebtSupplierExcel = ( +export const generateDebtSupplierExcel = async ( params: DebtSupplierExportExcelParams -): void => { +): Promise => { if (!params.data || params.data.length === 0) { return; } - const workbook = XLSX.utils.book_new(); + const workbook = new ExcelJS.Workbook(); - params.data.forEach((supplierReport) => { + const columns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Nomor PR', key: 'prNumber', width: 14 }, + { header: 'Nomor PO', key: 'poNumber', width: 14 }, + { header: 'Tanggal Terima/Bayar', key: 'receivedDate', width: 20 }, + { header: 'Tanggal PO', key: 'poDate', width: 10 }, + { header: 'Aging (Hari)', key: 'aging', width: 10 }, + { header: 'Area', key: 'area', width: 15 }, + { header: 'Gudang', key: 'warehouse', width: 15 }, + { header: 'Jatuh Tempo', key: 'dueDate', width: 12 }, + { header: 'Status Jatuh Tempo', key: 'dueStatus', width: 20 }, + { header: 'Nominal Pembelian (Rp)', key: 'totalPrice', width: 20 }, + { header: 'Pembayaran (Rp)', key: 'paymentPrice', width: 15 }, + { header: 'Sisa Saldo Hutang (Rp)', key: 'balance', width: 20 }, + { header: 'Status', key: 'status', width: 12 }, + { header: 'Nomor Perjalanan', key: 'travelNumber', width: 15 }, + ]; + + for (const supplierReport of params.data) { const supplierData = supplierReport.rows; const supplierName = supplierReport.supplier.name || 'Unknown Supplier'; - const excelData: { [key: string]: string | number }[] = [ - { - No: '', - 'Nomor PR': '', - 'Nomor PO': '', - 'Tanggal Terima/Bayar': '', - 'Tanggal PO': '', - 'Aging (Hari)': '', - Area: '', - Gudang: '', - 'Jatuh Tempo': '', - 'Status Jatuh Tempo': '', - 'Nominal Pembelian (Rp)': '', - 'Pembayaran (Rp)': '', - 'Sisa Saldo Hutang (Rp)': supplierReport.initial_balance || 0, - Status: '', - 'Nomor Perjalanan': '', - }, - ...supplierData.map((item, index) => ({ - No: index + 1, - 'Nomor PR': item.pr_number || '', - 'Nomor PO': item.po_number || '', - 'Tanggal Terima/Bayar': item.received_date + const worksheet = workbook.addWorksheet(supplierName.substring(0, 31)); + worksheet.columns = columns; + + // Add initial balance row + const initialRow = worksheet.addRow({ + no: '', + prNumber: '', + poNumber: '', + receivedDate: '', + poDate: '', + aging: '', + area: '', + warehouse: '', + dueDate: '', + dueStatus: '', + totalPrice: '', + paymentPrice: '', + balance: supplierReport.initial_balance || 0, + status: '', + travelNumber: '', + }); + + // Apply red color if initial balance is negative + const initialBalanceCell = initialRow.getCell('balance'); + if ( + typeof supplierReport.initial_balance === 'number' && + supplierReport.initial_balance < 0 + ) { + initialBalanceCell.font = { color: { argb: 'FFFF0000' } }; + } + + // Add data rows + supplierData.forEach((item, index) => { + const row = worksheet.addRow({ + no: index + 1, + prNumber: item.pr_number || '', + poNumber: item.po_number || '', + receivedDate: item.received_date ? item.received_date != '-' ? formatDate(item.received_date, 'MM/DD/YYYY') : '-' : '-', - 'Tanggal PO': item.po_date + poDate: item.po_date ? item.po_date != '-' ? formatDate(item.po_date, 'MM/DD/YYYY') : '-' : '-', - 'Aging (Hari)': item.aging || 0, - Area: item.area?.name || '', - Gudang: item.warehouse?.name || '', - 'Jatuh Tempo': item.due_date + aging: item.aging || 0, + area: item.area?.name || '', + warehouse: item.warehouse?.name || '', + dueDate: item.due_date ? item.due_date != '-' ? formatDate(item.due_date, 'MM/DD/YYYY') : '-' : '-', - 'Status Jatuh Tempo': item.due_status || '', - 'Nominal Pembelian (Rp)': item.total_price || 0, - 'Pembayaran (Rp)': item.payment_price || 0, - 'Sisa Saldo Hutang (Rp)': item.balance || 0, - Status: item.status || '', - 'Nomor Perjalanan': item.travel_number || '', - })), - ]; - - if (supplierReport.total) { - excelData.push({ - No: 'Total', - 'Nomor PR': '', - 'Nomor PO': '', - 'Tanggal Terima/Bayar': '', - 'Tanggal PO': '', - 'Aging (Hari)': supplierReport.total.aging || 0, - Area: '', - Gudang: '', - 'Jatuh Tempo': '', - 'Status Jatuh Tempo': '', - 'Nominal Pembelian (Rp)': supplierReport.total.total_price || 0, - 'Pembayaran (Rp)': supplierReport.total.payment_price || 0, - 'Sisa Saldo Hutang (Rp)': supplierReport.total.debt_price || 0, - Status: '', - 'Nomor Perjalanan': '', + dueStatus: item.due_status || '', + totalPrice: item.total_price || 0, + paymentPrice: item.payment_price || 0, + balance: item.balance || 0, + status: item.status || '', + travelNumber: item.travel_number || '', }); + + // Apply red color for negative values + const totalPriceCell = row.getCell('totalPrice'); + if (typeof item.total_price === 'number' && item.total_price < 0) { + totalPriceCell.font = { color: { argb: 'FFFF0000' } }; + } + + const paymentPriceCell = row.getCell('paymentPrice'); + if (typeof item.payment_price === 'number' && item.payment_price < 0) { + paymentPriceCell.font = { color: { argb: 'FFFF0000' } }; + } + + const balanceCell = row.getCell('balance'); + if (typeof item.balance === 'number' && item.balance < 0) { + balanceCell.font = { color: { argb: 'FFFF0000' } }; + } + }); + + // Add total row + if (supplierReport.total) { + const totalRow = worksheet.addRow({ + no: 'Total', + prNumber: '', + poNumber: '', + receivedDate: '', + poDate: '', + aging: supplierReport.total.aging || 0, + area: '', + warehouse: '', + dueDate: '', + dueStatus: '', + totalPrice: supplierReport.total.total_price || 0, + paymentPrice: supplierReport.total.payment_price || 0, + balance: supplierReport.total.debt_price || 0, + status: '', + travelNumber: '', + }); + + // Apply red color for negative totals + const totalPriceCell = totalRow.getCell('totalPrice'); + if ( + typeof supplierReport.total.total_price === 'number' && + supplierReport.total.total_price < 0 + ) { + totalPriceCell.font = { color: { argb: 'FFFF0000' } }; + } + + const paymentPriceCell = totalRow.getCell('paymentPrice'); + if ( + typeof supplierReport.total.payment_price === 'number' && + supplierReport.total.payment_price < 0 + ) { + paymentPriceCell.font = { color: { argb: 'FFFF0000' } }; + } + + const balanceCell = totalRow.getCell('balance'); + if ( + typeof supplierReport.total.debt_price === 'number' && + supplierReport.total.debt_price < 0 + ) { + balanceCell.font = { color: { argb: 'FFFF0000' } }; + } } - - const worksheet = XLSX.utils.json_to_sheet(excelData); - - const colWidths = [ - { wch: 5 }, // No - { wch: 10 }, // Nomor PR - { wch: 10 }, // Nomor PO - { wch: 20 }, // Tanggal Terima/Bayar - { wch: 10 }, // Tanggal PO - { wch: 10 }, // Aging - { wch: 15 }, // Area - { wch: 15 }, // Gudang - { wch: 12 }, // Jatuh Tempo - { wch: 20 }, // Status Jatuh Tempo - { wch: 20 }, // Nominal Pembelian (Rp) - { wch: 15 }, // Pembayaran (Rp) - { wch: 20 }, // Sisa Saldo Hutang (Rp) - { wch: 12 }, // Status - { wch: 15 }, // Nomor Perjalanan - ]; - worksheet['!cols'] = colWidths; - - const sheetName = - supplierName.length > 31 ? supplierName.substring(0, 31) : supplierName; - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); - }); + } const filename = `laporan-hutang-supplier-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; - XLSX.writeFile(workbook, filename); + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); }; diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 9fefa9c7..14fccffa 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -35,6 +35,8 @@ import ButtonFilter from '@/components/helper/ButtonFilter'; import Badge from '@/components/Badge'; import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; const dueStatus: Record = { 'Sudah Jatuh Tempo': 'error', @@ -671,7 +673,7 @@ const DebtSupplierTab = () => {
- {
-