mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-438): Add Excel and PDF export for Uniformity table
This commit is contained in:
@@ -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} />
|
||||
Export
|
||||
</Button>
|
||||
<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 > 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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user