Merge branch 'fix/debt-supplier' into 'development'

[FIX/FE] Adjust UI Debt Supplier

See merge request mbugroup/lti-web-client!177
This commit is contained in:
Rivaldi A N S
2026-01-15 07:43:03 +00:00
3 changed files with 291 additions and 59 deletions
@@ -18,6 +18,47 @@ Font.register({
src: 'helvetica', src: 'helvetica',
}); });
// Status color mappings (same as in DebtSupplierTab)
const dueStatusColors: Record<
string,
{ bg: string; text: string; border: string }
> = {
'Sudah Jatuh Tempo': { bg: '#FEE2E2', text: '#991B1B', border: '#F87171' }, // error/red
'Belum Jatuh Tempo': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
'Mendekati Jatuh Tempo': {
bg: '#FEF3C7',
text: '#92400E',
border: '#FBBF24',
}, // warning/yellow
};
const paymentStatusColors: Record<
string,
{ bg: string; text: string; border: string }
> = {
'Belum Lunas': { bg: '#FEF3C7', text: '#92400E', border: '#FBBF24' }, // warning/yellow
Lunas: { bg: '#DBEAFE', text: '#1E40AF', border: '#60A5FA' }, // primary/blue
Pembayaran: { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
};
/**
* Get badge style for PDF rendering
* @param statusText - The status text
* @param type - Type of status: 'due' or 'payment'
* @returns Style object with background and text colors
*/
const getPDFBadgeStyle = (
statusText: string,
type: 'due' | 'payment' = 'payment'
) => {
const colors =
type === 'due'
? dueStatusColors[statusText]
: paymentStatusColors[statusText];
return colors || { bg: '#F3F4F6', text: '#374151', border: '#D1D5DB' }; // neutral fallback
};
const pdfStyles = StyleSheet.create({ const pdfStyles = StyleSheet.create({
page: { page: {
fontSize: 10, fontSize: 10,
@@ -136,10 +177,40 @@ const pdfStyles = StyleSheet.create({
backgroundColor: '#F0F0F0', backgroundColor: '#F0F0F0',
fontWeight: 'bold', fontWeight: 'bold',
}, },
badge: {
paddingVertical: 2,
paddingHorizontal: 4,
borderRadius: 12,
fontSize: 5,
fontWeight: 'bold',
borderWidth: 1,
textAlign: 'center',
whiteSpace: 'nowrap',
},
parameterBadge: {
backgroundColor: '#F5F5F5',
color: '#333333',
padding: 4,
borderRadius: 4,
fontSize: 8,
marginRight: 8,
marginBottom: 4,
},
parameterContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 8,
},
}); });
interface DebtSupplierExportPDFParams { interface DebtSupplierExportPDFParams {
data: DebtSupplier[]; data: DebtSupplier[];
params?: {
supplier_name?: string;
start_date?: string;
end_date?: string;
filter_by?: string;
};
} }
const createPDFDocument = (params: DebtSupplierExportPDFParams) => { const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
@@ -157,9 +228,50 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
<Text style={pdfStyles.mainTitle}> <Text style={pdfStyles.mainTitle}>
Laporan &gt; Rekapitulasi Hutang ke Supplier Laporan &gt; Rekapitulasi Hutang ke Supplier
</Text> </Text>
<View style={pdfStyles.parameterContainer}>
<View style={pdfStyles.parameterBadge}>
<Text>
Periode:{' '}
{params.params?.start_date
? formatDate(params.params.start_date, 'DD MMM YYYY')
: '-'}{' '}
s.d{' '}
{params.params?.end_date
? formatDate(params.params.end_date, 'DD MMM YYYY')
: '-'}
</Text>
</View>
{params.params?.filter_by && (
<View style={pdfStyles.parameterBadge}>
<Text>
Filter Tanggal:{' '}
{params.params.filter_by === 'po_date'
? 'Tanggal PO'
: params.params.filter_by === 'received_date'
? 'Tanggal Terima'
: params.params.filter_by === 'due_date'
? 'Tanggal Jatuh Tempo'
: params.params.filter_by}
</Text>
</View>
)}
<View style={pdfStyles.parameterBadge}>
<Text>
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
</Text>
</View>
<View style={pdfStyles.parameterBadge}>
<Text>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</Text>
</View>
</View>
<Text style={pdfStyles.supplierTitle}> <Text style={pdfStyles.supplierTitle}>
{supplierReport.supplier.name} {supplierReport.supplier.name}
</Text> </Text>
<Text style={pdfStyles.supplierInfo}>
{supplierReport.supplier.category}
</Text>
</View> </View>
{/* Table */} {/* Table */}
@@ -193,7 +305,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Jatuh Tempo</Text> <Text>Jatuh Tempo</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 2 }]}>
<Text>Status Jatuh Tempo</Text> <Text>Status Jatuh Tempo</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
@@ -205,7 +317,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Sisa Saldo Hutang (Rp)</Text> <Text>Sisa Saldo Hutang (Rp)</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
<Text>Status</Text> <Text>Status</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
@@ -216,40 +328,40 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
{/* Initial Balance Row */} {/* Initial Balance Row */}
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}> <View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}> <View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text></Text> <Text></Text> {/* NO */}
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}> <View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text></Text> <Text></Text> {/* No. PR */}
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}> <View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text></Text> <Text></Text> {/* No. PO */}
</View> </View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}> <View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text></Text> <Text></Text> {/* Tgl Terima/Bayar */}
</View> </View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}> <View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text></Text> <Text></Text> {/* Tgl PO */}
</View> </View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}> <View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
<Text></Text> <Text></Text> {/* Aging */}
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> <Text></Text> {/* Area */}
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> <Text></Text> {/* Gudang */}
</View> </View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}> <View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text></Text> <Text></Text> {/* Jatuh Tempo */}
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 2 }]}>
<Text></Text> <Text></Text> {/* Status Jatuh Tempo */}
</View> </View>
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}> <View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
<Text></Text> <Text></Text> {/* Nominal Pembelian (Rp) */}
</View> </View>
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}> <View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
<Text></Text> <Text></Text> {/* Pembayaran (Rp) */}
</View> </View>
<View <View
style={[ style={[
@@ -261,14 +373,16 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
]} ]}
> >
<Text> <Text>
{' '}
{/* Sisa Saldo Hutang (Rp) */}
{formatCurrency(supplierReport.initial_balance || 0)} {formatCurrency(supplierReport.initial_balance || 0)}
</Text> </Text>
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text></Text> <Text></Text> {/* Status */}
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> <Text></Text> {/* No. Perjalanan */}
</View> </View>
</View> </View>
@@ -328,8 +442,32 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
: '-'} : '-'}
</Text> </Text>
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 2 }]}>
<Text>{item.due_status || '-'}</Text> {item.due_status && item.due_status !== '-' ? (
<View
style={[
pdfStyles.badge,
{
backgroundColor: getPDFBadgeStyle(
item.due_status,
'due'
).bg,
borderColor: getPDFBadgeStyle(item.due_status, 'due')
.border,
},
]}
>
<Text
style={{
color: getPDFBadgeStyle(item.due_status, 'due').text,
}}
>
{item.due_status}
</Text>
</View>
) : (
<Text>-</Text>
)}
</View> </View>
<View <View
style={[ style={[
@@ -361,8 +499,32 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
> >
<Text>{formatCurrency(item.balance)}</Text> <Text>{formatCurrency(item.balance)}</Text>
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text>{item.status || '-'}</Text> {item.status && item.status !== '-' ? (
<View
style={[
pdfStyles.badge,
{
backgroundColor: getPDFBadgeStyle(
item.status,
'payment'
).bg,
borderColor: getPDFBadgeStyle(item.status, 'payment')
.border,
},
]}
>
<Text
style={{
color: getPDFBadgeStyle(item.status, 'payment').text,
}}
>
{item.status}
</Text>
</View>
) : (
<Text>-</Text>
)}
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.travel_number || '-'}</Text> <Text>{item.travel_number || '-'}</Text>
@@ -400,7 +562,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> <Text></Text>
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 2 }]}>
<Text></Text> <Text></Text>
</View> </View>
<View <View
@@ -445,7 +607,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
> >
<Text>{formatCurrency(supplierReport.total.debt_price)}</Text> <Text>{formatCurrency(supplierReport.total.debt_price)}</Text>
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text></Text> <Text></Text>
</View> </View>
<View style={[pdfStyles.tableCellLast, { flex: 1 }]}> <View style={[pdfStyles.tableCellLast, { flex: 1 }]}>
@@ -64,7 +64,7 @@ export const generateDebtSupplierExcel = (
'Status Jatuh Tempo': item.due_status || '', 'Status Jatuh Tempo': item.due_status || '',
'Nominal Pembelian (Rp)': item.total_price || 0, 'Nominal Pembelian (Rp)': item.total_price || 0,
'Pembayaran (Rp)': item.payment_price || 0, 'Pembayaran (Rp)': item.payment_price || 0,
'Sisa Saldo Hutang (Rp)': item.debt_price || 0, 'Sisa Saldo Hutang (Rp)': item.balance || 0,
Status: item.status || '', Status: item.status || '',
'Nomor Perjalanan': item.travel_number || '', 'Nomor Perjalanan': item.travel_number || '',
})), })),
@@ -94,18 +94,18 @@ export const generateDebtSupplierExcel = (
const colWidths = [ const colWidths = [
{ wch: 5 }, // No { wch: 5 }, // No
{ wch: 15 }, // Nomor PR { wch: 10 }, // Nomor PR
{ wch: 15 }, // Nomor PO { wch: 10 }, // Nomor PO
{ wch: 15 }, // Tanggal Terima/Bayar { wch: 20 }, // Tanggal Terima/Bayar
{ wch: 15 }, // Tanggal PO { wch: 10 }, // Tanggal PO
{ wch: 12 }, // Aging { wch: 10 }, // Aging
{ wch: 15 }, // Area { wch: 15 }, // Area
{ wch: 15 }, // Gudang { wch: 15 }, // Gudang
{ wch: 18 }, // Jatuh Tempo { wch: 12 }, // Jatuh Tempo
{ wch: 18 }, // Status Jatuh Tempo { wch: 20 }, // Status Jatuh Tempo
{ wch: 15 }, // Nominal Pembelian (Rp) { wch: 20 }, // Nominal Pembelian (Rp)
{ wch: 15 }, // Pembayaran (Rp) { wch: 15 }, // Pembayaran (Rp)
{ wch: 15 }, // Sisa Saldo Hutang (Rp) { wch: 20 }, // Sisa Saldo Hutang (Rp)
{ wch: 12 }, // Status { wch: 12 }, // Status
{ wch: 15 }, // Nomor Perjalanan { wch: 15 }, // Nomor Perjalanan
]; ];
@@ -9,9 +9,9 @@ import SelectInput, {
import Menu from '@/components/menu/Menu'; import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem'; import MenuItem from '@/components/menu/MenuItem';
import Modal, { useModal } from '@/components/Modal'; import Modal, { useModal } from '@/components/Modal';
import Table from '@/components/Table'; import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { SupplierApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data';
import { import {
DebtRow, DebtRow,
@@ -31,10 +31,48 @@ import {
DebtSupplierFilterSchema, DebtSupplierFilterSchema,
DebtSupplierFilterType, DebtSupplierFilterType,
} from '@/components/pages/report/finance/filter/DebtSupplierFilter'; } from '@/components/pages/report/finance/filter/DebtSupplierFilter';
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
import ButtonFilter from '@/components/helper/ButtonFilter'; 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 { Supplier } from '@/types/api/master-data/supplier';
const dueStatus: Record<string, Color> = {
'Sudah Jatuh Tempo': 'error',
'Belum Jatuh Tempo': 'success',
'Mendekati Jatuh Tempo': 'warning',
};
const paymentStatus: Record<string, Color> = {
'Belum Lunas': 'warning',
Lunas: 'primary',
Pembayaran: 'success',
};
const getPillBadge = (
statusText: string,
type: 'due' | 'payment' = 'payment'
) => {
// Get color based on type
const color =
type === 'due'
? dueStatus[statusText] || 'neutral'
: paymentStatus[statusText] || 'neutral';
return (
<Badge
color={color as Color}
size='sm'
variant='soft'
className={{
badge: `py-2.5 px-2 font-medium text-base-content rounded-full border border-${color}`,
}}
statusIndicator
>
{statusText}
</Badge>
);
};
const DebtSupplierTab = () => { const DebtSupplierTab = () => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
@@ -212,7 +250,17 @@ const DebtSupplierTab = () => {
return; return;
} }
await generateDebtSupplierPDF({ data: allDataForExport }); await generateDebtSupplierPDF({
data: allDataForExport,
params: {
supplier_name: formik.values.supplierIds
?.map((v) => v.label)
.join(', '),
filter_by: formik.values.filterBy?.label,
start_date: formik.values.startDate || undefined,
end_date: formik.values.endDate || undefined,
},
});
toast.success('PDF berhasil dibuat dan diunduh.'); toast.success('PDF berhasil dibuat dan diunduh.');
} catch { } catch {
toast.error('Gagal membuat PDF. Silakan coba lagi.'); toast.error('Gagal membuat PDF. Silakan coba lagi.');
@@ -227,6 +275,7 @@ const DebtSupplierTab = () => {
header: 'No', header: 'No',
enableSorting: false, enableSorting: false,
cell: (props) => props.row.index, cell: (props) => props.row.index,
footer: () => 'Total',
}, },
{ {
id: 'pr_number', id: 'pr_number',
@@ -331,7 +380,7 @@ const DebtSupplierTab = () => {
enableSorting: false, enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.due_status; const value = props.row.original.due_status;
return value || '-'; return value ? (value != '-' ? getPillBadge(value, 'due') : '-') : '-';
}, },
}, },
{ {
@@ -407,7 +456,11 @@ const DebtSupplierTab = () => {
enableSorting: false, enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.status; const value = props.row.original.status;
return value || '-'; return value
? value != '-'
? getPillBadge(value, 'payment')
: '-'
: '-';
}, },
}, },
{ {
@@ -475,9 +528,15 @@ const DebtSupplierTab = () => {
<Card <Card
key={supplierReport.supplier.id} key={supplierReport.supplier.id}
title={supplierReport.supplier.name} title={supplierReport.supplier.name}
className={{ wrapper: 'w-full' }} className={{
wrapper: 'w-full !rounded-lg',
body: 'p-0 rounded-lg',
title:
'ps-2 pt-1 pb-1 font-normal text-md bg-primary text-white',
}}
variant='bordered' variant='bordered'
collapsible={true} collapsible={true}
defaultCollapsed={true}
> >
<Table <Table
data={[ data={[
@@ -491,34 +550,43 @@ const DebtSupplierTab = () => {
renderFooter={supplierReport.rows.length > 0} renderFooter={supplierReport.rows.length > 0}
className={{ className={{
containerClassName: 'w-full', containerClassName: 'w-full',
tableWrapperClassName: 'overflow-x-auto mt-4', tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm', headerColumnClassName: cn(
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', TABLE_DEFAULT_STYLING.headerColumnClassName,
headerColumnClassName: 'whitespace-nowrap'
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', ),
bodyRowClassName: bodyColumnClassName: cn(
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', TABLE_DEFAULT_STYLING.bodyColumnClassName,
bodyColumnClassName: 'whitespace-nowrap'
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', ),
tableFooterClassName: footerRowClassName: cn(
'bg-gray-100 font-semibold border border-gray-200', TABLE_DEFAULT_STYLING.footerRowClassName,
footerRowClassName: 'border-t-2 border-gray-300', 'bg-white'
footerColumnClassName: ),
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', footerColumnClassName: cn(
TABLE_DEFAULT_STYLING.footerColumnClassName,
'whitespace-nowrap p-3'
),
paginationClassName: 'hidden', paginationClassName: 'hidden',
}} }}
renderCustomRow={(row) => { renderCustomRow={(row) => {
if (row.index == 0) { if (row.index == 0) {
return ( return (
<tr <tr
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200' className={cn(TABLE_DEFAULT_STYLING.bodyRowClassName)}
key={row.index} key={row.index}
> >
<td <td
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap' className={cn(
TABLE_DEFAULT_STYLING.bodyColumnClassName
)}
colSpan={12} colSpan={12}
></td> ></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'> <td
className={cn(
TABLE_DEFAULT_STYLING.bodyColumnClassName
)}
>
<div <div
className={`text-right ${row.original.balance < 0 ? 'text-red-500' : ''}`} className={`text-right ${row.original.balance < 0 ? 'text-red-500' : ''}`}
> >
@@ -526,7 +594,9 @@ const DebtSupplierTab = () => {
</div> </div>
</td> </td>
<td <td
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap' className={cn(
TABLE_DEFAULT_STYLING.bodyColumnClassName
)}
colSpan={2} colSpan={2}
></td> ></td>
</tr> </tr>