fix(FE): adding color to negative value excel and change select UI

This commit is contained in:
randy-ar
2026-01-21 14:27:59 +07:00
parent f73672f65c
commit c7ffae68d8
4 changed files with 223 additions and 108 deletions
+70
View File
@@ -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<T = OptionType>
extends Omit<SelectInputProps<T>, 'closeMenuOnSelect' | 'optionComponent'> {
closeMenuOnSelect?: boolean;
}
const RadioOption = <
T extends OptionType,
IsMulti extends boolean,
Group extends GroupBase<T>,
>(
props: OptionProps<T, IsMulti, Group>
) => {
const { isSelected, label, innerRef, innerProps, className } = props;
return (
<div
ref={innerRef}
{...innerProps}
className={cn(
'flex items-center gap-2 px-3 py-2 cursor-pointer',
className
)}
>
<input
type='radio'
checked={isSelected}
onChange={() => null}
className='radio radio-sm radio-primary pointer-events-none'
/>
<label className='cursor-pointer flex-1 select-none'>{label}</label>
</div>
);
};
const SelectInputRadio = <T extends OptionType>(
props: SelectInputRadioProps<T>
) => {
const { closeMenuOnSelect = true, className, ...restProps } = props;
const customComponents = useMemo(() => {
return {
Option: RadioOption as typeof ReactSelectComponents.Option,
};
}, []);
return (
<SelectInput<T>
{...restProps}
closeMenuOnSelect={closeMenuOnSelect}
className={{
...className,
select: cn(className?.select, 'select-radio'),
}}
components={customComponents}
/>
);
};
export default SelectInputRadio;
@@ -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<void> => {
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);
};
@@ -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<string, Color> = {
'Sudah Jatuh Tempo': 'error',
@@ -671,7 +673,7 @@ const DebtSupplierTab = () => {
</div>
<div>
<SelectInput
<SelectInputCheckbox
label='Supplier'
placeholder='Pilih Supplier'
isMulti
@@ -696,7 +698,7 @@ const DebtSupplierTab = () => {
</div>
<div>
<SelectInput
<SelectInputRadio
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}