mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 15:25:46 +00:00
Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu
This commit is contained in:
@@ -18,6 +18,47 @@ Font.register({
|
||||
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({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
@@ -136,10 +177,40 @@ const pdfStyles = StyleSheet.create({
|
||||
backgroundColor: '#F0F0F0',
|
||||
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 {
|
||||
data: DebtSupplier[];
|
||||
params?: {
|
||||
supplier_name?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
filter_by?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
@@ -157,9 +228,50 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
<Text style={pdfStyles.mainTitle}>
|
||||
Laporan > Rekapitulasi Hutang ke Supplier
|
||||
</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}>
|
||||
{supplierReport.supplier.name}
|
||||
</Text>
|
||||
<Text style={pdfStyles.supplierInfo}>
|
||||
{supplierReport.supplier.category}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Table */}
|
||||
@@ -193,7 +305,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Jatuh Tempo</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 2 }]}>
|
||||
<Text>Status Jatuh Tempo</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||
@@ -205,7 +317,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||
<Text>Sisa Saldo Hutang (Rp)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||
<Text>Status</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
@@ -216,40 +328,40 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
{/* Initial Balance Row */}
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
|
||||
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* NO */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* No. PR */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* No. PO */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* Tgl Terima/Bayar */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* Tgl PO */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* Aging */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* Area */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* Gudang */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* Jatuh Tempo */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
|
||||
<Text></Text> {/* Status Jatuh Tempo */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* Nominal Pembelian (Rp) */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* Pembayaran (Rp) */}
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
@@ -261,14 +373,16 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{' '}
|
||||
{/* Sisa Saldo Hutang (Rp) */}
|
||||
{formatCurrency(supplierReport.initial_balance || 0)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
<Text></Text> {/* Status */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
<Text></Text> {/* No. Perjalanan */}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -328,8 +442,32 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.due_status || '-'}</Text>
|
||||
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
|
||||
{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
|
||||
style={[
|
||||
@@ -361,8 +499,32 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
>
|
||||
<Text>{formatCurrency(item.balance)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.status || '-'}</Text>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
{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 style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.travel_number || '-'}</Text>
|
||||
@@ -400,7 +562,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
@@ -445,7 +607,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
>
|
||||
<Text>{formatCurrency(supplierReport.total.debt_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellLast, { flex: 1 }]}>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const generateDebtSupplierExcel = (
|
||||
'Status Jatuh Tempo': item.due_status || '',
|
||||
'Nominal Pembelian (Rp)': item.total_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 || '',
|
||||
'Nomor Perjalanan': item.travel_number || '',
|
||||
})),
|
||||
@@ -94,18 +94,18 @@ export const generateDebtSupplierExcel = (
|
||||
|
||||
const colWidths = [
|
||||
{ wch: 5 }, // No
|
||||
{ wch: 15 }, // Nomor PR
|
||||
{ wch: 15 }, // Nomor PO
|
||||
{ wch: 15 }, // Tanggal Terima/Bayar
|
||||
{ wch: 15 }, // Tanggal PO
|
||||
{ wch: 12 }, // Aging
|
||||
{ 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: 18 }, // Jatuh Tempo
|
||||
{ wch: 18 }, // Status Jatuh Tempo
|
||||
{ wch: 15 }, // Nominal Pembelian (Rp)
|
||||
{ wch: 12 }, // Jatuh Tempo
|
||||
{ wch: 20 }, // Status Jatuh Tempo
|
||||
{ wch: 20 }, // Nominal Pembelian (Rp)
|
||||
{ wch: 15 }, // Pembayaran (Rp)
|
||||
{ wch: 15 }, // Sisa Saldo Hutang (Rp)
|
||||
{ wch: 20 }, // Sisa Saldo Hutang (Rp)
|
||||
{ wch: 12 }, // Status
|
||||
{ wch: 15 }, // Nomor Perjalanan
|
||||
];
|
||||
|
||||
@@ -9,9 +9,9 @@ import SelectInput, {
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
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 { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { SupplierApi } from '@/services/api/master-data';
|
||||
import {
|
||||
DebtRow,
|
||||
@@ -31,10 +31,48 @@ import {
|
||||
DebtSupplierFilterSchema,
|
||||
DebtSupplierFilterType,
|
||||
} from '@/components/pages/report/finance/filter/DebtSupplierFilter';
|
||||
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
import Badge from '@/components/Badge';
|
||||
import { Color } from '@/types/theme';
|
||||
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 = () => {
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||
@@ -212,7 +250,17 @@ const DebtSupplierTab = () => {
|
||||
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.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
@@ -227,6 +275,7 @@ const DebtSupplierTab = () => {
|
||||
header: 'No',
|
||||
enableSorting: false,
|
||||
cell: (props) => props.row.index,
|
||||
footer: () => 'Total',
|
||||
},
|
||||
{
|
||||
id: 'pr_number',
|
||||
@@ -331,7 +380,7 @@ const DebtSupplierTab = () => {
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.due_status;
|
||||
return value || '-';
|
||||
return value ? (value != '-' ? getPillBadge(value, 'due') : '-') : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -407,7 +456,11 @@ const DebtSupplierTab = () => {
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.status;
|
||||
return value || '-';
|
||||
return value
|
||||
? value != '-'
|
||||
? getPillBadge(value, 'payment')
|
||||
: '-'
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -475,9 +528,15 @@ const DebtSupplierTab = () => {
|
||||
<Card
|
||||
key={supplierReport.supplier.id}
|
||||
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'
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
>
|
||||
<Table
|
||||
data={[
|
||||
@@ -491,34 +550,43 @@ const DebtSupplierTab = () => {
|
||||
renderFooter={supplierReport.rows.length > 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',
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
headerColumnClassName: cn(
|
||||
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
||||
'whitespace-nowrap'
|
||||
),
|
||||
bodyColumnClassName: cn(
|
||||
TABLE_DEFAULT_STYLING.bodyColumnClassName,
|
||||
'whitespace-nowrap'
|
||||
),
|
||||
footerRowClassName: cn(
|
||||
TABLE_DEFAULT_STYLING.footerRowClassName,
|
||||
'bg-white'
|
||||
),
|
||||
footerColumnClassName: cn(
|
||||
TABLE_DEFAULT_STYLING.footerColumnClassName,
|
||||
'whitespace-nowrap p-3'
|
||||
),
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
renderCustomRow={(row) => {
|
||||
if (row.index == 0) {
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
<td
|
||||
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
||||
className={cn(
|
||||
TABLE_DEFAULT_STYLING.bodyColumnClassName
|
||||
)}
|
||||
colSpan={12}
|
||||
></td>
|
||||
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'>
|
||||
<td
|
||||
className={cn(
|
||||
TABLE_DEFAULT_STYLING.bodyColumnClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={`text-right ${row.original.balance < 0 ? 'text-red-500' : ''}`}
|
||||
>
|
||||
@@ -526,7 +594,9 @@ const DebtSupplierTab = () => {
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
||||
className={cn(
|
||||
TABLE_DEFAULT_STYLING.bodyColumnClassName
|
||||
)}
|
||||
colSpan={2}
|
||||
></td>
|
||||
</tr>
|
||||
|
||||
@@ -21,10 +21,18 @@ import {
|
||||
ProjectFlockApi,
|
||||
ProjectFlockKandangApi,
|
||||
} from '@/services/api/production';
|
||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import {
|
||||
BaseProjectFlockKandang,
|
||||
ProjectFlockKandang,
|
||||
} from '@/types/api/production/project-flock-kandang';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import Pagination from '@/components/Pagination';
|
||||
import { ProductionResultReportApi } from '@/services/api/report/production-result';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { httpClient } from '@/services/http/client';
|
||||
import { ProductionResult } from '@/types/api/report/production-result';
|
||||
import ProductionResultReportPDF from './ProductionResultReportPDF';
|
||||
import { pdf } from '@react-pdf/renderer';
|
||||
|
||||
const ProductionResultContent = () => {
|
||||
const [projectFlockKandangs, setProjectFlockKandangs] = useState<
|
||||
@@ -49,6 +57,8 @@ const ProductionResultContent = () => {
|
||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||
useState(false);
|
||||
|
||||
const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false);
|
||||
|
||||
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
@@ -158,6 +168,87 @@ const ProductionResultContent = () => {
|
||||
setIsLoadingExportingToExcel(false);
|
||||
};
|
||||
|
||||
const exportToPdfHandler = async () => {
|
||||
setIsLoadingExportingToPdf(true);
|
||||
|
||||
try {
|
||||
let projectFlockKandangsData: BaseProjectFlockKandang[] = [];
|
||||
|
||||
if (selectedProjectFlockKandang) {
|
||||
const projectFlockKandangResponse =
|
||||
await ProjectFlockKandangApi.getSingle(
|
||||
selectedProjectFlockKandang?.value as number
|
||||
);
|
||||
|
||||
projectFlockKandangsData = isResponseSuccess(
|
||||
projectFlockKandangResponse
|
||||
)
|
||||
? [projectFlockKandangResponse.data]
|
||||
: [];
|
||||
} else {
|
||||
const projectFlockKandangsResponse =
|
||||
await ProjectFlockKandangApi.getAll({
|
||||
area_id: selectedArea?.value,
|
||||
project_flock_id: selectedProjectFlock?.value,
|
||||
});
|
||||
|
||||
projectFlockKandangsData = isResponseSuccess(
|
||||
projectFlockKandangsResponse
|
||||
)
|
||||
? projectFlockKandangsResponse.data
|
||||
: [];
|
||||
}
|
||||
|
||||
const mappedProductionResults: {
|
||||
projectFlockKandang: BaseProjectFlockKandang;
|
||||
productionResult: ProductionResult[] | null;
|
||||
}[] = await Promise.all(
|
||||
projectFlockKandangsData.map(async (projectFlockKandang) => {
|
||||
const getProductionResultPath = `${ProductionResultReportApi.basePath}/${projectFlockKandang.id}?page=1&limit=100`;
|
||||
const getProductionResultRes = await httpClient<
|
||||
BaseApiResponse<ProductionResult[]>
|
||||
>(getProductionResultPath);
|
||||
|
||||
return {
|
||||
projectFlockKandang,
|
||||
productionResult: isResponseSuccess(getProductionResultRes)
|
||||
? getProductionResultRes.data
|
||||
: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (mappedProductionResults.length === 0) {
|
||||
toast.error('Tidak ada data untuk diexport.');
|
||||
setIsLoadingExportingToPdf(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const openPdf = async () => {
|
||||
const productionResultPdfBlob = await pdf(
|
||||
<ProductionResultReportPDF
|
||||
mappedProductionResults={mappedProductionResults}
|
||||
/>
|
||||
).toBlob();
|
||||
|
||||
const productionResultReportPdfUrl = URL.createObjectURL(
|
||||
productionResultPdfBlob
|
||||
);
|
||||
window.open(productionResultReportPdfUrl, '_blank');
|
||||
};
|
||||
|
||||
await openPdf();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.');
|
||||
}
|
||||
// await ProductionResultReportApi.exportProductionResultToPdf(
|
||||
// projectFlockKandangs
|
||||
// );
|
||||
|
||||
setIsLoadingExportingToPdf(false);
|
||||
};
|
||||
|
||||
const searchHandler = async () => {
|
||||
setProjectFlockKandangs(null);
|
||||
setIsLoadingSearch(true);
|
||||
@@ -355,6 +446,13 @@ const ProductionResultContent = () => {
|
||||
onClick={exportToExcelHandler}
|
||||
className='text-nowrap'
|
||||
/>
|
||||
<MenuItem
|
||||
title='Export to PDF'
|
||||
icon='icon-park-outline:file-pdf-one'
|
||||
isLoading={isLoadingExportingToPdf}
|
||||
onClick={exportToPdfHandler}
|
||||
className='text-nowrap'
|
||||
/>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Document,
|
||||
Page,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Image,
|
||||
} from '@react-pdf/renderer';
|
||||
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
import { ProductionResult } from '@/types/api/report/production-result';
|
||||
|
||||
type MappedProductionResultsItem = {
|
||||
projectFlockKandang: BaseProjectFlockKandang;
|
||||
productionResult: ProductionResult[] | null;
|
||||
};
|
||||
|
||||
interface ProductionResultReportPDFProps {
|
||||
mappedProductionResults?: MappedProductionResultsItem[];
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
paddingTop: 24,
|
||||
paddingBottom: 52,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
|
||||
companyInfoHeader: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyLogo: {
|
||||
width: 64,
|
||||
height: 'auto',
|
||||
},
|
||||
companyInfoHeaderDate: {
|
||||
paddingTop: 8,
|
||||
fontSize: 10,
|
||||
},
|
||||
companyName: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
companyAddress: {
|
||||
fontSize: 8,
|
||||
maxWidth: 420,
|
||||
marginBottom: 10,
|
||||
},
|
||||
doubleDivider: {
|
||||
width: '100%',
|
||||
height: 6,
|
||||
borderTopWidth: 2,
|
||||
borderTopColor: '#000',
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: '#000',
|
||||
},
|
||||
|
||||
title: {
|
||||
marginTop: 14,
|
||||
fontSize: 14,
|
||||
lineHeight: '150%',
|
||||
textAlign: 'center',
|
||||
fontFamily: 'Times-Roman',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
footer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
position: 'absolute',
|
||||
fontSize: 8,
|
||||
bottom: 22,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
color: 'grey',
|
||||
},
|
||||
|
||||
section: {
|
||||
marginTop: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000',
|
||||
padding: 8,
|
||||
},
|
||||
|
||||
sectionHeader: {
|
||||
marginBottom: 6,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 8,
|
||||
color: '#444',
|
||||
},
|
||||
|
||||
// Simple grid table (label/value pairs)
|
||||
grid: {
|
||||
width: '100%',
|
||||
borderWidth: 1,
|
||||
borderColor: '#000',
|
||||
},
|
||||
gridRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000',
|
||||
},
|
||||
gridRowLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
gridCellLabel: {
|
||||
width: '40%',
|
||||
paddingVertical: 3,
|
||||
paddingHorizontal: 6,
|
||||
fontSize: 8,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
gridCellValue: {
|
||||
width: '60%',
|
||||
paddingVertical: 3,
|
||||
paddingHorizontal: 6,
|
||||
fontSize: 8,
|
||||
textAlign: 'right',
|
||||
},
|
||||
|
||||
// Subsection headings
|
||||
groupTitle: {
|
||||
marginTop: 8,
|
||||
marginBottom: 4,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
emptyText: {
|
||||
fontSize: 8,
|
||||
color: '#666',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
});
|
||||
|
||||
function safeNum(v: unknown): number {
|
||||
const n = typeof v === 'number' ? v : Number(v);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
function valueText(v: unknown) {
|
||||
if (v === null || v === undefined) return '-';
|
||||
if (typeof v === 'number') return formatNumber(v);
|
||||
return String(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render label/value table for one ProductionResult.
|
||||
* Uses a compact grid to keep page readable.
|
||||
*/
|
||||
function ProductionResultGrid({ pr }: { pr: ProductionResult }) {
|
||||
const rows: Array<[string, string]> = [
|
||||
['WOA', valueText(pr.woa)],
|
||||
|
||||
// BW
|
||||
['BW', valueText(pr.bw)],
|
||||
['Std BW', valueText(pr.std_bw)],
|
||||
['Uniformity', valueText(pr.uniformity)],
|
||||
['Std Uniformity', valueText(pr.std_uniformity)],
|
||||
|
||||
// Dep
|
||||
['Dep Kum', valueText(pr.dep_kum)],
|
||||
['Dep Std', valueText(pr.dep_std)],
|
||||
|
||||
// Butiran
|
||||
['Butiran Utuh', valueText(pr.butiran_utuh)],
|
||||
['Butiran Putih', valueText(pr.butiran_putih)],
|
||||
['Butiran Retak', valueText(pr.butiran_retak)],
|
||||
['Butiran Pecah', valueText(pr.butiran_pecah)],
|
||||
['Butiran Jumlah', valueText(pr.butiran_jumlah)],
|
||||
['Total Butir', valueText(pr.total_butir)],
|
||||
|
||||
// Kg
|
||||
['Kg Utuh', valueText(pr.kg_utuh)],
|
||||
['Kg Putih', valueText(pr.kg_putih)],
|
||||
['Kg Retak', valueText(pr.kg_retak)],
|
||||
['Kg Pecah', valueText(pr.kg_pecah)],
|
||||
['Kg Jumlah', valueText(pr.kg_jumlah)],
|
||||
['Total Kg', valueText(pr.total_kg)],
|
||||
|
||||
// %
|
||||
['% Utuh', valueText(pr.persen_utuh)],
|
||||
['% Putih', valueText(pr.persen_putih)],
|
||||
['% Retak', valueText(pr.persen_retak)],
|
||||
['% Pecah', valueText(pr.persen_pecah)],
|
||||
|
||||
// Produksi
|
||||
['HD', valueText(pr.hd)],
|
||||
['HD Std', valueText(pr.hd_std)],
|
||||
['FI', valueText(pr.fi)],
|
||||
['FI Std', valueText(pr.fi_std)],
|
||||
['EM', valueText(pr.em)],
|
||||
['EM Std', valueText(pr.em_std)],
|
||||
['EW', valueText(pr.ew)],
|
||||
['EW Std', valueText(pr.ew_std)],
|
||||
['FCR', valueText(pr.fcr)],
|
||||
['FCR Std', valueText(pr.fcr_std)],
|
||||
['HH', valueText(pr.hh)],
|
||||
['HH Std', valueText(pr.hh_std)],
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={styles.grid}>
|
||||
{rows.map(([label, value], idx) => {
|
||||
const isLast = idx === rows.length - 1;
|
||||
return (
|
||||
<View
|
||||
key={label}
|
||||
style={[styles.gridRow, ...(isLast ? [styles.gridRowLast] : [])]}
|
||||
>
|
||||
<Text style={styles.gridCellLabel}>{label}</Text>
|
||||
<Text style={styles.gridCellValue}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are multiple ProductionResult entries for a kandang,
|
||||
* we show them sequentially with a small header per result.
|
||||
*
|
||||
* You can later change this to render only the latest WOA, or group by week.
|
||||
*/
|
||||
function ProductionResultList({
|
||||
productionResults,
|
||||
}: {
|
||||
productionResults: ProductionResult[];
|
||||
}) {
|
||||
return (
|
||||
<View>
|
||||
{productionResults.map((pr, idx) => {
|
||||
const kandangName =
|
||||
pr.project_flock?.kandang?.name ||
|
||||
pr.project_flock?.kandang?.id?.toString() ||
|
||||
'';
|
||||
|
||||
// Optional: show a compact subheader
|
||||
const headerLeft = `Data #${idx + 1}`;
|
||||
const headerRight =
|
||||
kandangName && pr.woa !== undefined
|
||||
? `${kandangName} • WOA ${safeNum(pr.woa)}`
|
||||
: pr.woa !== undefined
|
||||
? `WOA ${safeNum(pr.woa)}`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<View
|
||||
key={`${pr.project_flock?.id ?? 'pf'}-${idx}`}
|
||||
style={{ marginTop: idx === 0 ? 0 : 10 }}
|
||||
wrap={false}
|
||||
>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>{headerLeft}</Text>
|
||||
<Text style={styles.sectionSubtitle}>{headerRight}</Text>
|
||||
</View>
|
||||
|
||||
<ProductionResultGrid pr={pr} />
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Main PDF Component
|
||||
*/
|
||||
const ProductionResultReportPDF = ({
|
||||
mappedProductionResults = [],
|
||||
}: ProductionResultReportPDFProps) => {
|
||||
return (
|
||||
<Document>
|
||||
<Page style={styles.page} size='A4'>
|
||||
{/* Header */}
|
||||
<View>
|
||||
<View style={styles.companyInfoHeader}>
|
||||
<Image style={styles.companyLogo} src='/assets/img/lti-logo.png' />
|
||||
<Text style={styles.companyInfoHeaderDate}>
|
||||
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={styles.companyName}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
<Text style={styles.companyAddress}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
|
||||
<View style={styles.doubleDivider} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.title}>Laporan Production Result</Text>
|
||||
|
||||
{/* Sections per ProjectFlockKandang */}
|
||||
{mappedProductionResults.length === 0 ? (
|
||||
<View style={{ marginTop: 16 }}>
|
||||
<Text style={styles.emptyText}>Tidak ada data.</Text>
|
||||
</View>
|
||||
) : (
|
||||
mappedProductionResults.map((item, idx) => {
|
||||
const pfk = item.projectFlockKandang;
|
||||
|
||||
// Try to display meaningful identifiers.
|
||||
// Adjust these fields based on your real BaseProjectFlockKandang structure.
|
||||
const kandangName =
|
||||
pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`;
|
||||
|
||||
const projectName = pfk?.project_flock?.name ?? '';
|
||||
|
||||
const locationName = pfk?.project_flock?.location?.name ?? '';
|
||||
|
||||
const areaName = pfk?.project_flock?.area?.name ?? '';
|
||||
|
||||
return (
|
||||
<View
|
||||
key={`pfk-${pfk?.id ?? idx}`}
|
||||
style={styles.section}
|
||||
break={idx > 0} // each kandang starts on a new page for clarity
|
||||
>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
{projectName
|
||||
? `${projectName} • ${kandangName}`
|
||||
: kandangName}
|
||||
</Text>
|
||||
<Text style={styles.sectionSubtitle}>
|
||||
{[areaName, locationName].filter(Boolean).join(' • ')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{item.productionResult && item.productionResult.length > 0 ? (
|
||||
<ProductionResultList
|
||||
productionResults={item.productionResult}
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.emptyText}>
|
||||
Tidak ada production result untuk kandang ini.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer} fixed>
|
||||
<Text
|
||||
render={({ pageNumber, totalPages }) =>
|
||||
`${pageNumber} / ${totalPages}`
|
||||
}
|
||||
fixed
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionResultReportPDF;
|
||||
Reference in New Issue
Block a user