diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 80ee34f4..ff7fbb15 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -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 = {}; @@ -688,10 +801,24 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { Filter - + + + Export + + } + align='end' + > + + + + + diff --git a/src/components/pages/uniformity/export/UniformityExportExcel.tsx b/src/components/pages/uniformity/export/UniformityExportExcel.tsx new file mode 100644 index 00000000..cc9a2248 --- /dev/null +++ b/src/components/pages/uniformity/export/UniformityExportExcel.tsx @@ -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); +}; diff --git a/src/components/pages/uniformity/export/UniformityExportPDF.tsx b/src/components/pages/uniformity/export/UniformityExportPDF.tsx new file mode 100644 index 00000000..b22efcfd --- /dev/null +++ b/src/components/pages/uniformity/export/UniformityExportPDF.tsx @@ -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 ( + + + {/* Title and Parameters */} + + Production > Uniformity + + {getParameterText(params).map((param, index) => ( + + {param} + + ))} + + + + {/* Table */} + + {/* Table Header */} + + + No + + + Lokasi + + + Project Flock + + + Kandang + + + Tanggal (Week) + + + Status + + + Uniformity (%) + + + CV (%) + + + Chick Qty + + + Uniform Qty + + + Mean Up + + + Mean Down + + + + {/* Table Body */} + {data.map((item: Uniformity, index: number) => ( + + + {index + 1} + + + {item.location_name || '-'} + + + {item.flock_name || '-'} + + + {item.kandang_name || '-'} + + + + {formatDate(item.applied_at, 'DD MMM YYYY')} (Week {item.week} + ) + + + + + {getStatusText(item.status)} + + + + {formatNumber(item.uniformity)} + + + {formatNumber(item.cv)} + + + {formatNumber(item.chick_qty_of_weight)} + + + {formatNumber(item.uniform_qty)} + + + {formatNumber(item.mean_up)} + + + {formatNumber(item.mean_down)} + + + ))} + + + + ); +}; + +export const generateUniformityPDF = async ( + data: UniformityExportPDFParams['data'], + params: UniformityExportPDFParams['params'] +): Promise => { + 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; + } +};