From 781a5ca0d975b0d5969403fddfe7261247f5d971 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:04 +0700 Subject: [PATCH 1/5] chore: use real permission for daily checklist menu --- src/config/constant.ts | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/config/constant.ts b/src/config/constant.ts index d3832613..b3621c8f 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -10,61 +10,65 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Daily Checklist', link: '/daily-checklist', icon: 'heroicons-outline:clipboard-check', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: [ + 'lti.daily_checklist.dashboard.list', + 'lti.daily_checklist.create', + 'lti.daily_checklist.list', + 'lti.daily_checklist.detail', + 'lti.daily_checklist.reports', + 'lti.daily_checklist.master_data.employee', + 'lti.daily_checklist.master_data.activity', + 'lti.daily_checklist.master_data.configuration', + ], submenu: [ { text: 'Dashboard', link: '/daily-checklist/dashboard', icon: 'lucide:layout-dashboard', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.dashboard.list'], }, { text: 'Daily Checklist', link: '/daily-checklist/daily-checklist', icon: 'lucide:clipboard-check', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.create'], }, { text: 'Daftar Daily Checklist', link: '/daily-checklist/list-daily-checklist', icon: 'lucide:circle-check', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.list'], }, { text: 'Laporan', link: '/daily-checklist/reports', icon: 'lucide:file-text', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.reports'], }, { text: 'Master Data', link: '/daily-checklist/master-data', icon: 'lucide:database', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: [ + 'lti.daily_checklist.master_data.employee', + 'lti.daily_checklist.master_data.activity', + 'lti.daily_checklist.master_data.configuration', + ], submenu: [ { text: 'Employee (ABK)', link: '/daily-checklist/master-data/employee', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.master_data.employee'], }, { text: 'Aktivitas', link: '/daily-checklist/master-data/activity', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.master_data.activity'], }, { text: 'Konfigurasi', link: '/daily-checklist/master-data/configuration', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.master_data.configuration'], }, ], }, From 8f55ced55a3c89029a77493f128999ec9edba26f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:17 +0700 Subject: [PATCH 2/5] feat: add export to pdf functionality --- .../ProductionResultContent.tsx | 102 +++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/production-result/ProductionResultContent.tsx b/src/components/pages/report/production-result/ProductionResultContent.tsx index 7820ff53..28d334e8 100644 --- a/src/components/pages/report/production-result/ProductionResultContent.tsx +++ b/src/components/pages/report/production-result/ProductionResultContent.tsx @@ -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(null); const [selectedLocation, setSelectedLocation] = useState( 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 + >(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( + + ).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' /> + From e15b7e11d3bd7075d76dd2ffe418da6a3b48133e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:33 +0700 Subject: [PATCH 3/5] feat: create ProductionResultReportPDF component --- .../ProductionResultReportPDF.tsx | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 src/components/pages/report/production-result/ProductionResultReportPDF.tsx diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx new file mode 100644 index 00000000..9bc27c4b --- /dev/null +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -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 ( + + {rows.map(([label, value], idx) => { + const isLast = idx === rows.length - 1; + return ( + + {label} + {value} + + ); + })} + + ); +} + +/** + * 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 ( + + {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 ( + + + {headerLeft} + {headerRight} + + + + + ); + })} + + ); +} + +/** + * ✅ Main PDF Component + */ +const ProductionResultReportPDF = ({ + mappedProductionResults = [], +}: ProductionResultReportPDFProps) => { + return ( + + + {/* Header */} + + + + + {formatDate(Date.now(), 'DD MMMM YYYY')} + + + + + PT LUMBUNG TELUR INDONESIA + + SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. + Cipedes, Kec. Sukajadi, Kota Bandung 40162 + + + + + + + Laporan Production Result + + {/* Sections per ProjectFlockKandang */} + {mappedProductionResults.length === 0 ? ( + + Tidak ada data. + + ) : ( + 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 ( + 0} // each kandang starts on a new page for clarity + > + + + {projectName + ? `${projectName} • ${kandangName}` + : kandangName} + + + {[areaName, locationName].filter(Boolean).join(' • ')} + + + + {item.productionResult && item.productionResult.length > 0 ? ( + + ) : ( + + Tidak ada production result untuk kandang ini. + + )} + + ); + }) + )} + + {/* Footer */} + + + `${pageNumber} / ${totalPages}` + } + fixed + /> + + + + ); +}; + +export default ProductionResultReportPDF; From bd64694c7324619a6394fe801edf3f1e1b436e29 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:56:30 +0700 Subject: [PATCH 4/5] feat: implement closing sapronak per kandang --- .../pages/closing/ClosingIncomingSapronaksTable.tsx | 6 +++++- .../pages/closing/ClosingOutgoingSapronaksTable.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx index 53e45710..eda7e756 100644 --- a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx +++ b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx @@ -1,6 +1,7 @@ 'use client'; import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; @@ -23,6 +24,9 @@ interface ClosingIncomingSapronaksTableProps { const ClosingIncomingSapronaksTable = ({ projectFlockId, }: ClosingIncomingSapronaksTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { state: tableFilterState, updateFilter, @@ -43,7 +47,7 @@ const ClosingIncomingSapronaksTable = ({ const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming`, + `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`, ClosingApi.getAllIncomingSapronakFetcher, { keepPreviousData: true, diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx index 5662cff1..ac918561 100644 --- a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx @@ -1,6 +1,7 @@ 'use client'; import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; @@ -23,6 +24,9 @@ interface ClosingOutgoingSapronaksTableProps { const ClosingOutgoingSapronaksTable = ({ projectFlockId, }: ClosingOutgoingSapronaksTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { state: tableFilterState, updateFilter, @@ -43,7 +47,7 @@ const ClosingOutgoingSapronaksTable = ({ const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing`, + `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`, ClosingApi.getAllOutgoingSapronakFetcher, { keepPreviousData: true, From fce2cfee736efea0828bcca11e62fbf85e6a49a5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:56:47 +0700 Subject: [PATCH 5/5] feat: implement closing production data per kandang --- .../pages/closing/ClosingProductionDataTabContent.tsx | 8 ++++++-- src/services/api/closing.ts | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx index 0f15d5b9..9295d283 100644 --- a/src/components/pages/closing/ClosingProductionDataTabContent.tsx +++ b/src/components/pages/closing/ClosingProductionDataTabContent.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -12,9 +13,12 @@ interface ClosingProductionDataTabContentProps { const ClosingProductionDataTabContent = ({ projectFlockId, }: ClosingProductionDataTabContentProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { data: productionData, isLoading } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/production-data`, - () => ClosingApi.getProductionData(projectFlockId) + `${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`, + () => ClosingApi.getProductionData(projectFlockId, Number(kandangId)) ); if (isLoading) { diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index b2ba2b8f..323e09e8 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -91,10 +91,11 @@ export class ClosingApiService extends BaseApiService { } async getProductionData( - id: number + id: number, + kandangId?: number ): Promise | undefined> { try { - const getProductionDataPath = `${this.basePath}/${id}/production-data`; + const getProductionDataPath = `${this.basePath}/${id}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`; const getProductionDataRes = await httpClient< BaseApiResponse >(getProductionDataPath);