mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 14:55:44 +00:00
feat(FE-338): Slicing UI Halaman Reporting BOP & API integration & refactor debounce input: adding useEffect for sync value
This commit is contained in:
@@ -24,6 +24,11 @@ const DebouncedTextInput = (props: DebouncedTextInputProps) => {
|
||||
setInternalChangeEvent(e);
|
||||
};
|
||||
|
||||
// Sync internal value with external value prop changes (e.g., from reset)
|
||||
useEffect(() => {
|
||||
setInternalValue(props.value);
|
||||
}, [props.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedChangeEvent) {
|
||||
onChange?.(debouncedChangeEvent);
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
import Badge from '@/components/Badge';
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
||||
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useMemo, useState } from 'react';
|
||||
import ReportExpenseExport from '@/components/pages/report/expense/pdf/ReportExpenseExport';
|
||||
|
||||
const ReportExpenseTable = ({
|
||||
reportExpenses,
|
||||
onSearch,
|
||||
}: {
|
||||
reportExpenses: ReportExpense[];
|
||||
onSearch: (params: {
|
||||
locationId: string | null;
|
||||
supplierId: string | null;
|
||||
kandangId: string | null;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
category: string | null;
|
||||
period: string | number;
|
||||
search: string;
|
||||
}) => void;
|
||||
}) => {
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [selectedSupplier, setSelectedSupplier] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [selectedCategory, setSelectedCategory] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [search, setSearch] = useState('');
|
||||
const [startDate, setStartDate] = useState<string | null>(null);
|
||||
const [endDate, setEndDate] = useState<string | null>(null);
|
||||
const [period, setPeriod] = useState<number | string>('');
|
||||
|
||||
const { options: optionsLocation, isLoadingOptions: isLoadingLocation } =
|
||||
useSelect(`/master-data/locations`, 'id', 'name');
|
||||
const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } =
|
||||
useSelect(`/master-data/suppliers`, 'id', 'name');
|
||||
const { options: optionsKandang, isLoadingOptions: isLoadingKandang } =
|
||||
useSelect(`/master-data/kandangs`, 'id', 'name', '', {
|
||||
location_id: selectedLocation?.value.toString() || '',
|
||||
});
|
||||
|
||||
const columns = useMemo((): ColumnDef<ReportExpense>[] => {
|
||||
return [
|
||||
{
|
||||
header: 'No',
|
||||
accessorFn: (_, index) => index + 1,
|
||||
},
|
||||
{
|
||||
header: 'No. PO',
|
||||
accessorKey: 'po_number',
|
||||
},
|
||||
{
|
||||
header: 'No. Referensi',
|
||||
accessorKey: 'reference_number',
|
||||
},
|
||||
{
|
||||
header: 'Tanggal Realisasi',
|
||||
accessorKey: 'realization_date',
|
||||
cell: ({ row }) => {
|
||||
return formatDate(row.original.realization_date, 'DD MMM, YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Tanggal Transaksi',
|
||||
accessorKey: 'transaction_date',
|
||||
cell: ({ row }) => {
|
||||
return formatDate(row.original.transaction_date, 'DD MMM, YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kategori',
|
||||
accessorKey: 'category',
|
||||
},
|
||||
{
|
||||
header: 'Supplier',
|
||||
accessorFn: (row) => row.supplier.name,
|
||||
},
|
||||
{
|
||||
header: 'Lokasi',
|
||||
accessorFn: (row) => row.location.name,
|
||||
},
|
||||
{
|
||||
header: 'Kandang',
|
||||
accessorFn: (row) => row.kandang.name,
|
||||
},
|
||||
{
|
||||
header: 'Pengajuan',
|
||||
columns: [
|
||||
{
|
||||
header: 'Qty',
|
||||
id: 'qty_pengajuan',
|
||||
accessorFn: (row) => row.pengajuan.qty,
|
||||
cell: ({ row }) =>
|
||||
row.original.pengajuan.qty.toLocaleString('id-ID'),
|
||||
},
|
||||
{
|
||||
header: 'Harga',
|
||||
id: 'harga_pengajuan',
|
||||
accessorFn: (row) => row.pengajuan.price,
|
||||
cell: ({ row }) => formatCurrency(row.original.pengajuan.price),
|
||||
},
|
||||
{
|
||||
header: 'Total',
|
||||
id: 'total_pengajuan',
|
||||
accessorFn: (row) => row.pengajuan.qty * row.pengajuan.price,
|
||||
cell: ({ row }) => {
|
||||
const total =
|
||||
row.original.pengajuan.qty * row.original.pengajuan.price;
|
||||
return formatCurrency(total);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Realisasi',
|
||||
columns: [
|
||||
{
|
||||
header: 'Qty',
|
||||
id: 'qty_realisasi',
|
||||
accessorFn: (row) => row.realisasi.qty,
|
||||
cell: ({ row }) =>
|
||||
row.original.realisasi.qty.toLocaleString('id-ID'),
|
||||
},
|
||||
{
|
||||
header: 'Harga',
|
||||
id: 'harga_realisasi',
|
||||
accessorFn: (row) => row.realisasi.price,
|
||||
cell: ({ row }) => formatCurrency(row.original.realisasi.price),
|
||||
},
|
||||
{
|
||||
header: 'Total',
|
||||
id: 'total_realisasi',
|
||||
accessorFn: (row) => row.realisasi.qty * row.realisasi.price,
|
||||
cell: ({ row }) => {
|
||||
const total =
|
||||
row.original.realisasi.qty * row.original.realisasi.price;
|
||||
return formatCurrency(total);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Status Pencairan',
|
||||
cell: (props) => (
|
||||
<RealizationStatusBadge
|
||||
approval={props.row.original.latest_approval}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Status BOP',
|
||||
cell: (props) => (
|
||||
<ExpenseStatusBadge approval={props.row.original.latest_approval} />
|
||||
),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
// Handle Search
|
||||
const handleSearch = () => {
|
||||
onSearch({
|
||||
search,
|
||||
period,
|
||||
startDate,
|
||||
endDate,
|
||||
locationId: selectedLocation?.value.toString() ?? '',
|
||||
kandangId: selectedKandang?.value.toString() ?? '',
|
||||
supplierId: selectedSupplier?.value.toString() ?? '',
|
||||
category: selectedCategory?.value.toString() ?? '',
|
||||
});
|
||||
};
|
||||
const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
onSearch({
|
||||
search: e.target.value,
|
||||
period,
|
||||
startDate,
|
||||
endDate,
|
||||
locationId: selectedLocation?.value.toString() ?? '',
|
||||
kandangId: selectedKandang?.value.toString() ?? '',
|
||||
supplierId: selectedSupplier?.value.toString() ?? '',
|
||||
category: selectedCategory?.value.toString() ?? '',
|
||||
});
|
||||
};
|
||||
const handleReset = () => {
|
||||
setSearch('');
|
||||
setPeriod('');
|
||||
setStartDate('');
|
||||
setEndDate('');
|
||||
setSelectedLocation(null);
|
||||
setSelectedKandang(null);
|
||||
setSelectedSupplier(null);
|
||||
setSelectedCategory(null);
|
||||
onSearch({
|
||||
search: '',
|
||||
period: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
locationId: '',
|
||||
kandangId: '',
|
||||
supplierId: '',
|
||||
category: '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Card
|
||||
title='Laporan Biaya Operasional'
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
footer={
|
||||
<div className='flex flex-row items-center justify-between gap-2'>
|
||||
<div>
|
||||
<ReportExpenseExport data={reportExpenses} />
|
||||
</div>
|
||||
<div className='flex flex-row items-center gap-2'>
|
||||
<Button className='min-w-24' onClick={handleSearch}>
|
||||
Cari
|
||||
</Button>
|
||||
<Button
|
||||
className='min-w-24'
|
||||
color='warning'
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Lokasi'
|
||||
options={optionsLocation}
|
||||
isLoading={isLoadingLocation}
|
||||
placeholder='Lokasi'
|
||||
value={selectedLocation}
|
||||
onChange={(option) => {
|
||||
setSelectedLocation(option as OptionType);
|
||||
setSelectedKandang(null);
|
||||
}}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Kandang'
|
||||
options={optionsKandang}
|
||||
isLoading={isLoadingKandang}
|
||||
placeholder='Kandang'
|
||||
value={selectedKandang}
|
||||
onChange={(option) => setSelectedKandang(option as OptionType)}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Supplier'
|
||||
options={optionsSupplier}
|
||||
isLoading={isLoadingSupplier}
|
||||
placeholder='Supplier'
|
||||
value={selectedSupplier}
|
||||
onChange={(option) => setSelectedSupplier(option as OptionType)}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Kategori'
|
||||
options={[
|
||||
{
|
||||
value: 'BOP',
|
||||
label: 'BOP',
|
||||
},
|
||||
{
|
||||
value: 'NON BOP',
|
||||
label: 'Non BOP',
|
||||
},
|
||||
]}
|
||||
placeholder='Kategori'
|
||||
value={selectedCategory}
|
||||
onChange={(option) => setSelectedCategory(option as OptionType)}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Periode'
|
||||
value={period}
|
||||
onChange={(e) => setPeriod(e.target.value)}
|
||||
name='periode'
|
||||
placeholder='Periode'
|
||||
/>
|
||||
<DateInput
|
||||
label='Tanggal Mulai'
|
||||
value={startDate as string}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
name='start_date'
|
||||
placeholder='Tanggal Mulai'
|
||||
/>
|
||||
<DateInput
|
||||
label='Tanggal Selesai'
|
||||
value={endDate as string}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
name='end_date'
|
||||
placeholder='Tanggal Selesai'
|
||||
/>
|
||||
<DebouncedTextInput
|
||||
label='Cari'
|
||||
name='search'
|
||||
value={search}
|
||||
onChange={handleSearchInput}
|
||||
placeholder='Cari'
|
||||
startAdornment={<Icon icon='mdi:magnify' width={24} height={24} />}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Table<ReportExpense>
|
||||
columns={columns}
|
||||
data={reportExpenses}
|
||||
className={{
|
||||
headerRowClassName: cn(TABLE_DEFAULT_STYLING, 'whitespace-nowrap'),
|
||||
bodyRowClassName: cn(TABLE_DEFAULT_STYLING, 'whitespace-nowrap'),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportExpenseTable;
|
||||
@@ -0,0 +1,420 @@
|
||||
import Button from '@/components/Button';
|
||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||
import pdfStyles from '@/components/pages/report/expense/pdf/styles/ReportExpenseStyles';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface ReportExpenseExportProps {
|
||||
data: ReportExpense[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ReportExpenseExport = ({ data }: ReportExpenseExportProps) => {
|
||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!data || data.length === 0) {
|
||||
toast.error('No report expense data available');
|
||||
return;
|
||||
}
|
||||
setIsGeneratingPDF(true);
|
||||
try {
|
||||
const blob = await pdf(<PDFDocument data={data} />).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `Laporan-BOP-${formatDate(new Date(), 'DD-MMM-YYYY')}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
toast.error('Failed to generate PDF. Please try again.');
|
||||
return error;
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
color='error'
|
||||
className='min-w-32'
|
||||
onClick={handleDownloadPDF}
|
||||
isLoading={isGeneratingPDF}
|
||||
>
|
||||
<Icon icon='mdi:file-pdf-box' width={20} height={20} />
|
||||
Export PDF
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportExpenseExport;
|
||||
|
||||
const PDFDocument = ({ data }: { data: ReportExpense[] }) => {
|
||||
// Group data by supplier
|
||||
const groupedBySupplier = useMemo(() => {
|
||||
const groups: Record<string, ReportExpense[]> = {};
|
||||
data.forEach((item) => {
|
||||
const supplierName = item.supplier.name;
|
||||
if (!groups[supplierName]) {
|
||||
groups[supplierName] = [];
|
||||
}
|
||||
groups[supplierName].push(item);
|
||||
});
|
||||
return groups;
|
||||
}, [data]);
|
||||
|
||||
// Calculate grand totals
|
||||
const grandTotals = useMemo(() => {
|
||||
return data.reduce(
|
||||
(acc, item) => {
|
||||
const pengajuanTotal = item.pengajuan.qty * item.pengajuan.price;
|
||||
const realisasiTotal = item.realisasi.qty * item.realisasi.price;
|
||||
return {
|
||||
pengajuan: acc.pengajuan + pengajuanTotal,
|
||||
realisasi: acc.realisasi + realisasiTotal,
|
||||
};
|
||||
},
|
||||
{ pengajuan: 0, realisasi: 0 }
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size='A4' orientation='landscape' style={pdfStyles.page}>
|
||||
{/* Header Section */}
|
||||
<View style={pdfStyles.header}>
|
||||
<Image
|
||||
src={'https://placehold.co/120x30/png'}
|
||||
style={pdfStyles.logo}
|
||||
id={'mbu-logo'}
|
||||
/>
|
||||
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
<Text style={pdfStyles.address}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
<View style={pdfStyles.divider} />
|
||||
</View>
|
||||
|
||||
{/* Report Title */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.title}>LAPORAN BIAYA OPERASIONAL</Text>
|
||||
<View style={pdfStyles.poInfo}>
|
||||
<Text>Tanggal Cetak: {formatDate(new Date(), 'DD MMM YYYY')}</Text>
|
||||
<Text>Total Data: {data.length} transaksi</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Grouped Tables by Supplier */}
|
||||
{Object.entries(groupedBySupplier).map(
|
||||
([supplierName, items], groupIndex) => {
|
||||
const supplierTotals = items.reduce(
|
||||
(acc, item) => {
|
||||
const pengajuanTotal =
|
||||
item.pengajuan.qty * item.pengajuan.price;
|
||||
const realisasiTotal =
|
||||
item.realisasi.qty * item.realisasi.price;
|
||||
return {
|
||||
pengajuan: acc.pengajuan + pengajuanTotal,
|
||||
realisasi: acc.realisasi + realisasiTotal,
|
||||
};
|
||||
},
|
||||
{ pengajuan: 0, realisasi: 0 }
|
||||
);
|
||||
|
||||
return (
|
||||
<View key={groupIndex} style={pdfStyles.allocationSection}>
|
||||
{/* Supplier Header */}
|
||||
<Text style={pdfStyles.sectionTitle}>{supplierName}</Text>
|
||||
|
||||
{/* Table */}
|
||||
<View style={pdfStyles.table}>
|
||||
{/* Table Header */}
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>No</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>No. PO</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>No. Referensi</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Tgl Realisasi</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Tgl Transaksi</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Kategori</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Lokasi</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Kandang</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Qty Pengajuan</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Harga Pengajuan</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Total Pengajuan</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Qty Realisasi</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Harga Realisasi</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Total Realisasi</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Status Pencairan</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Status BOP</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Table Body */}
|
||||
{items.map((item, index) => {
|
||||
const pengajuanTotal =
|
||||
item.pengajuan.qty * item.pengajuan.price;
|
||||
const realisasiTotal =
|
||||
item.realisasi.qty * item.realisasi.price;
|
||||
|
||||
return (
|
||||
<View key={index} style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{index + 1}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.po_number}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.reference_number}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>
|
||||
{formatDate(item.realization_date, 'DD MMM YY')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>
|
||||
{formatDate(item.transaction_date, 'DD MMM YY')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.category}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.location.name}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.kandang.name}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>
|
||||
{item.pengajuan.qty.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatCurrency(item.pengajuan.price)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatCurrency(pengajuanTotal)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>
|
||||
{item.realisasi.qty.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatCurrency(item.realisasi.price)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatCurrency(realisasiTotal)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 7,
|
||||
backgroundColor:
|
||||
item.latest_approval.step_number === 3
|
||||
? '#dcfce7'
|
||||
: '#fef3c7',
|
||||
padding: 2,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{item.latest_approval.step_number === 3
|
||||
? 'Lunas'
|
||||
: 'Belum Lunas'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 7,
|
||||
backgroundColor:
|
||||
item.latest_approval.action === 'APPROVED'
|
||||
? '#dcfce7'
|
||||
: item.latest_approval.action === 'REJECTED'
|
||||
? '#fee2e2'
|
||||
: '#fef3c7',
|
||||
padding: 2,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{item.latest_approval.action}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Supplier Subtotal Row */}
|
||||
<View style={pdfStyles.grandTotalRow}>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>Subtotal</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{formatCurrency(supplierTotals.pengajuan)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>Subtotal</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{formatCurrency(supplierTotals.realisasi)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
)}
|
||||
|
||||
{/* Grand Total Section */}
|
||||
<View style={pdfStyles.allocationSection}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: '30%',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#000000',
|
||||
borderTopStyle: 'solid',
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: '#000000',
|
||||
borderLeftStyle: 'solid',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={pdfStyles.innerRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
GRAND TOTAL PENGAJUAN
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{formatCurrency(grandTotals.pengajuan)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.innerRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
GRAND TOTAL REALISASI
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{formatCurrency(grandTotals.realisasi)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={pdfStyles.footer}>
|
||||
<View style={pdfStyles.footerCompany}>
|
||||
<Text>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
import { StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 18,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
logo: {
|
||||
width: 120,
|
||||
height: 30,
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyInfo: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
address: {
|
||||
fontSize: 7,
|
||||
color: '#666666',
|
||||
maxWidth: 400,
|
||||
marginBottom: 10,
|
||||
},
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
marginBottom: 15,
|
||||
},
|
||||
titleSection: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 20,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
flex: 3,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
poInfo: {
|
||||
flex: 1,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
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: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
tableCellLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellHeaderLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
grandTotalRow: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#000000',
|
||||
borderTopStyle: 'solid',
|
||||
},
|
||||
grandTotalLabel: {
|
||||
flex: 3,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
},
|
||||
grandTotalValue: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
allocationSection: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
allocationTable: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerTable: {
|
||||
marginTop: 5,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
innerCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
innerCellLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
},
|
||||
innerCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
innerCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 3,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
footer: {
|
||||
marginTop: 30,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
footerCompany: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
flex: 1,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
specialInstructionTable: {
|
||||
width: '60%',
|
||||
maxWidth: 300,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default pdfStyles;
|
||||
Reference in New Issue
Block a user