feat(FE-438): Add Excel and PDF export for Uniformity table

This commit is contained in:
rstubryan
2025-12-30 11:37:08 +07:00
parent 9be09ae281
commit 7290f242f4
3 changed files with 556 additions and 4 deletions
@@ -39,6 +39,11 @@ import {
getStatusIndicatorColor,
getStatusText,
} from '@/components/pages/uniformity/uniformity-utils';
import { generateUniformityPDF } from '@/components/pages/uniformity/export/UniformityExportPDF';
import { generateUniformityExcel } from '@/components/pages/uniformity/export/UniformityExportExcel';
import Dropdown from '@/components/Dropdown';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
const isUniformityLocked = (uniformity: Uniformity): boolean => {
return uniformity.status === 'APPROVED' || uniformity.status === 'REJECTED';
@@ -174,6 +179,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
const singleDeleteModal = useModal();
const successModal = useModal();
@@ -532,6 +540,111 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
}
}, [selectedRowIds, refreshUniformities, bulkRejectModal]);
// ===== EXPORT HANDLERS =====
const uniformityExport = useCallback(async (): Promise<
Uniformity[] | null
> => {
const queryParams = new URLSearchParams();
if (filterProjectFlockKandangId) {
queryParams.append(
'project_flock_kandang_id',
filterProjectFlockKandangId.toString()
);
}
if (filterStartDate) {
queryParams.append('start_date', filterStartDate);
}
if (filterEndDate) {
queryParams.append('end_date', filterEndDate);
}
queryParams.append('limit', '10000');
queryParams.append('page', '1');
const queryString = queryParams.toString();
const url = `${UniformityApi.basePath}${queryString ? `?${queryString}` : ''}`;
const response = await UniformityApi.getAllFetcher(url);
return isResponseSuccess(response) ? response.data : null;
}, [filterProjectFlockKandangId, filterStartDate, filterEndDate]);
const handleExportExcel = useCallback(async () => {
setIsExcelExportLoading(true);
try {
const allDataForExport = await uniformityExport();
if (!allDataForExport || allDataForExport.length === 0) {
toast.error('Tidak ada data untuk diekspor.');
return;
}
const locationName = filterLocation?.label || 'Semua Lokasi';
const projectFlockName =
filterProjectFlock?.label || 'Semua Project Flock';
const kandangName = filterKandang?.label || 'Semua Kandang';
generateUniformityExcel(allDataForExport, {
location_name: locationName,
project_flock_name: projectFlockName,
kandang_name: kandangName,
start_date: filterStartDate,
end_date: filterEndDate,
});
toast.success('Excel berhasil dibuat dan diunduh.');
} catch {
toast.error('Gagal membuat Excel. Silakan coba lagi.');
} finally {
setIsExcelExportLoading(false);
}
}, [
uniformityExport,
filterLocation,
filterProjectFlock,
filterKandang,
filterStartDate,
filterEndDate,
]);
const handleExportPDF = useCallback(async () => {
setIsPdfExportLoading(true);
try {
const allDataForExport = await uniformityExport();
if (!allDataForExport || allDataForExport.length === 0) {
toast.error('Tidak ada data untuk diekspor.');
return;
}
const locationName = filterLocation?.label || 'Semua Lokasi';
const projectFlockName =
filterProjectFlock?.label || 'Semua Project Flock';
const kandangName = filterKandang?.label || 'Semua Kandang';
await generateUniformityPDF(allDataForExport, {
location_name: locationName,
project_flock_name: projectFlockName,
kandang_name: kandangName,
start_date: filterStartDate,
end_date: filterEndDate,
});
toast.success('PDF berhasil dibuat dan diunduh.');
} catch {
toast.error('Gagal membuat PDF. Silakan coba lagi.');
} finally {
setIsPdfExportLoading(false);
}
}, [
uniformityExport,
filterLocation,
filterProjectFlock,
filterKandang,
filterStartDate,
filterEndDate,
]);
useEffect(() => {
if (isResponseSuccess(uniformities) && uniformities.data) {
const newSelection: Record<string, boolean> = {};
@@ -688,10 +801,24 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
Filter
</Button>
<Button variant='outline'>
<Icon icon='heroicons:cloud-arrow-down' width={18} height={18} />
<Dropdown
trigger={
<Button variant='outline' isLoading={isAnyExportLoading}>
<Icon
icon='heroicons:cloud-arrow-down'
width={18}
height={18}
/>
Export
</Button>
}
align='end'
>
<Menu>
<MenuItem title='Excel' onClick={handleExportExcel} />
<MenuItem title='PDF' onClick={handleExportPDF} />
</Menu>
</Dropdown>
</div>
</section>
@@ -0,0 +1,86 @@
'use client';
import * as XLSX from 'xlsx';
import type { Uniformity } from '@/types/api/uniformity/uniformity';
import { formatDate, formatNumber } from '@/lib/helper';
interface UniformityExportExcelParams {
data: Uniformity[];
params: {
location_name?: string;
project_flock_name?: string;
kandang_name?: string;
start_date?: string;
end_date?: string;
};
}
const getStatusText = (status: string) => {
switch (status) {
case 'APPROVED':
return 'Disetujui';
case 'REJECTED':
return 'Ditolak';
case 'CREATED':
return 'Pengajuan';
default:
return status;
}
};
export const generateUniformityExcel = (
data: UniformityExportExcelParams['data'],
params: UniformityExportExcelParams['params']
): void => {
if (!data || data.length === 0) {
return;
}
const excelData: { [key: string]: string | number }[] = data.map(
(item: Uniformity, index: number) => ({
No: index + 1,
Lokasi: item.location_name || '',
'Project Flock': item.flock_name || '',
Kandang: item.kandang_name || '',
Tanggal: formatDate(item.applied_at, 'DD MMM YYYY'),
Minggu: item.week || 0,
Status: getStatusText(item.status),
'Uniformity (%)': formatNumber(item.uniformity),
'CV (%)': formatNumber(item.cv),
'Chick Qty': formatNumber(item.chick_qty_of_weight),
'Uniform Qty': formatNumber(item.uniform_qty),
'Mean Up': formatNumber(item.mean_up),
'Mean Down': formatNumber(item.mean_down),
})
);
const worksheet = XLSX.utils.json_to_sheet(excelData);
const colWidths = [
{ wch: 6 }, // No
{ wch: 25 }, // Lokasi
{ wch: 20 }, // Project Flock
{ wch: 15 }, // Kandang
{ wch: 15 }, // Tanggal
{ wch: 10 }, // Minggu
{ wch: 12 }, // Status
{ wch: 15 }, // Uniformity (%)
{ wch: 10 }, // CV (%)
{ wch: 12 }, // Chick Qty
{ wch: 12 }, // Uniform Qty
{ wch: 12 }, // Mean Up
{ wch: 12 }, // Mean Down
];
worksheet['!cols'] = colWidths;
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Uniformity');
const period =
params.start_date && params.end_date
? `${params.start_date}-${params.end_date}`
: formatDate(new Date(), 'YYYY-MM-DD');
const filename = `laporan-uniformity-${period}.xlsx`;
XLSX.writeFile(workbook, filename);
};
@@ -0,0 +1,339 @@
'use client';
import {
Page,
Text,
View,
Document,
StyleSheet,
Font,
pdf,
} from '@react-pdf/renderer';
import { formatDate, formatNumber } from '@/lib/helper';
import type { Uniformity } from '@/types/api/uniformity/uniformity';
Font.register({
family: 'Helvetica',
src: 'helvetica',
});
const pdfStyles = StyleSheet.create({
page: {
fontSize: 10,
fontFamily: 'Helvetica',
padding: 20,
backgroundColor: '#FFFFFF',
},
titleSection: {
marginBottom: 10,
},
mainTitle: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
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: 4,
fontSize: 8,
textAlign: 'left',
},
tableCellHeader: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 8,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
paddingVertical: 12,
textAlign: 'center',
},
tableCellHeaderRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 8,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
textAlign: 'right',
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
paddingVertical: 12,
},
tableCellRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 8,
textAlign: 'right',
},
tableCellCenter: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 8,
textAlign: 'center',
},
tableBorderBottom: {
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
},
badge: {
backgroundColor: '#1f74bf',
color: '#FFFFFF',
padding: 2,
borderRadius: 2,
fontSize: 7,
fontWeight: 'bold',
alignSelf: 'center',
},
parameterBadge: {
backgroundColor: '#F5F5F5',
color: '#333333',
padding: 4,
borderRadius: 4,
fontSize: 8,
marginRight: 8,
marginBottom: 4,
},
parameterContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 8,
},
});
interface UniformityExportPDFParams {
data: Uniformity[];
params: {
location_name?: string;
project_flock_name?: string;
kandang_name?: string;
start_date?: string;
end_date?: string;
};
}
const getParameterText = (params: UniformityExportPDFParams['params']) => {
const paramsText = [];
if (params.location_name && params.location_name !== 'Semua Lokasi') {
paramsText.push(`Lokasi: ${params.location_name}`);
}
if (
params.project_flock_name &&
params.project_flock_name !== 'Semua Project Flock'
) {
paramsText.push(`Project Flock: ${params.project_flock_name}`);
}
if (params.kandang_name && params.kandang_name !== 'Semua Kandang') {
paramsText.push(`Kandang: ${params.kandang_name}`);
}
if (params.start_date && params.end_date) {
const formattedStartDate = formatDate(params.start_date, 'DD MMM YYYY');
const formattedEndDate = formatDate(params.end_date, 'DD MMM YYYY');
paramsText.push(`Periode: ${formattedStartDate} - ${formattedEndDate}`);
} else if (params.start_date) {
const formattedStartDate = formatDate(params.start_date, 'DD MMM YYYY');
paramsText.push(`Tanggal Mulai: ${formattedStartDate}`);
} else if (params.end_date) {
const formattedEndDate = formatDate(params.end_date, 'DD MMM YYYY');
paramsText.push(`Tanggal Akhir: ${formattedEndDate}`);
}
const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm');
paramsText.push(`Dicetak: ${currentDate}`);
return paramsText;
};
const getStatusText = (status: string) => {
switch (status) {
case 'APPROVED':
return 'Disetujui';
case 'REJECTED':
return 'Ditolak';
case 'CREATED':
return 'Pengajuan';
default:
return status;
}
};
const createPDFDocument = (
data: UniformityExportPDFParams['data'],
params: UniformityExportPDFParams['params']
) => {
return (
<Document>
<Page size='A4' orientation='landscape' style={pdfStyles.page}>
{/* Title and Parameters */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.mainTitle}>Production &gt; Uniformity</Text>
<View style={pdfStyles.parameterContainer}>
{getParameterText(params).map((param, index) => (
<View key={index} style={pdfStyles.parameterBadge}>
<Text>{param}</Text>
</View>
))}
</View>
</View>
{/* Table */}
<View style={pdfStyles.table}>
{/* Table Header */}
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
<Text>No</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
<Text>Lokasi</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
<Text>Project Flock</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
<Text>Kandang</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
<Text>Tanggal (Week)</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Status</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
<Text>Uniformity (%)</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
<Text>CV (%)</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Chick Qty</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
<Text>Uniform Qty</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
<Text>Mean Up</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
<Text>Mean Down</Text>
</View>
</View>
{/* Table Body */}
{data.map((item: Uniformity, index: number) => (
<View
key={index}
style={[
pdfStyles.tableRow,
index < data.length - 1 ? pdfStyles.tableBorderBottom : {},
]}
>
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
<Text>{index + 1}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text>{item.location_name || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text>{item.flock_name || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text>{item.kandang_name || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text>
{formatDate(item.applied_at, 'DD MMM YYYY')} (Week {item.week}
)
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<View style={pdfStyles.badge}>
<Text>{getStatusText(item.status)}</Text>
</View>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
<Text>{formatNumber(item.uniformity)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
<Text>{formatNumber(item.cv)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatNumber(item.chick_qty_of_weight)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
<Text>{formatNumber(item.uniform_qty)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
<Text>{formatNumber(item.mean_up)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
<Text>{formatNumber(item.mean_down)}</Text>
</View>
</View>
))}
</View>
</Page>
</Document>
);
};
export const generateUniformityPDF = async (
data: UniformityExportPDFParams['data'],
params: UniformityExportPDFParams['params']
): Promise<void> => {
const PDFDocument = createPDFDocument(data, params);
try {
const blob = await pdf(PDFDocument).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
const period =
params.start_date && params.end_date
? `${params.start_date}-${params.end_date}`
: formatDate(new Date(), 'YYYY-MM-DD');
link.download = `laporan-uniformity-${period}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
throw error;
}
};