import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api, errorMessage } from '../../services/apiClient.ts'; import { useFarm } from '../../context/FarmContext.tsx'; import { buildInMemoryReport } from '../../utils/buildInMemoryReport.ts'; import { downloadReportExcel } from '../../utils/reportExcel.ts'; import { exportElementToPdf } from '../../utils/pdfExport.ts'; import { countingTotals, feedInTotals, feedOutTotals, feedUseTotals, } from '../../utils/reportTotals.ts'; import { cycleDayForDate, formatDateId, formatDateRange, formatNumber, formatRatio, } from '../../utils/format.ts'; import { buildReportSummary, formatSummaryValue } from '../../utils/reportSummary.ts'; import type { DashboardReportPayload, DashboardReportType } from '../../types/dashboardReport.ts'; import { Banner } from '../ui/Feedback.tsx'; type Props = { open: boolean; reportType: DashboardReportType; onClose: () => void; }; const cell = (value: number | string | null | undefined, suffix = ''): string => { if (value === null || value === undefined || value === '') return 'N/A'; if (typeof value === 'number' && !Number.isFinite(value)) return 'N/A'; return `${value}${suffix}`; }; const dashCell = (value: number | null | undefined): string => value == null || !Number.isFinite(value) ? '-' : String(value); const accuracyCell = (value: number | null | undefined): string => value == null || !Number.isFinite(value) ? 'N/A' : `${value.toFixed(1)}%`; const ratioCell = (value: number | null | undefined): string => value == null || !Number.isFinite(value) ? 'N/A' : formatRatio(Number(value), 3); const qtyCell = (value: number | null | undefined, maxDecimals = 0, suffix = ''): string => { if (value == null || !Number.isFinite(value)) return 'N/A'; return `${formatNumber(Number(value), maxDecimals, 0, true)}${suffix}`; }; const harvestKgCell = (grams: number | null | undefined): string => { if (grams == null || !Number.isFinite(grams)) return 'N/A'; return `${formatNumber(Number(grams) / 1000, 1)} kg`; }; const ReportPreviewOverlay: React.FC = ({ open, reportType, onClose }) => { const { selectedCycle, selectedKandang, selectedSite } = useFarm(); const isEndCycle = reportType === 'end_cycle'; const [draftDay, setDraftDay] = useState(0); const [payload, setPayload] = useState(null); const [generating, setGenerating] = useState(false); const [error, setError] = useState(null); const printRef = useRef(null); const latestDay = selectedCycle ? Math.max(0, selectedCycle.current_day) : 0; const endCycleDay = selectedCycle ? Math.min( selectedCycle.total_days, selectedCycle.end_date ? cycleDayForDate(selectedCycle.start_date, selectedCycle.end_date) : Math.max(0, (selectedCycle.total_days || 1) - 1) ) : 0; const effectiveUpToDay = isEndCycle ? endCycleDay : draftDay; const dayOptions = useMemo( () => Array.from({ length: latestDay + 1 }, (_, day) => day), [latestDay] ); useEffect(() => { if (!open) return; setPayload(null); setError(null); setGenerating(false); setDraftDay(isEndCycle ? endCycleDay : latestDay); }, [open, isEndCycle, latestDay, endCycleDay]); const generate = useCallback(async () => { if (!selectedCycle || !selectedKandang) return; setGenerating(true); setError(null); try { const cycleId = selectedCycle.id; const [countings, manuals, weights, karungs, kpis] = await Promise.all([ api.countings.list({ cycle_id: cycleId }), api.manualInputs.list({ cycle_id: cycleId }), api.weights.list({ cycle_id: cycleId }), api.karungs.list({ cycle_id: cycleId }), api.kpis.list({ cycle_id: cycleId }), ]); const next = buildInMemoryReport({ cycle: selectedCycle, kandangId: selectedKandang.id, kandangName: selectedKandang.kandang_name, area: selectedSite?.site_name ?? '', reportType, countings, manuals, weights, karungs, kpis, upToDay: effectiveUpToDay, }); setPayload(next); } catch (err) { setError(errorMessage(err)); } finally { setGenerating(false); } }, [reportType, selectedCycle, selectedKandang, selectedSite, effectiveUpToDay]); const downloadPdf = useCallback(async () => { if (!printRef.current || !payload) return; const period = isEndCycle ? 'Akhir-Siklus' : `Harian-Hari-${payload.upToDay}`; const filename = `Laporan-${period}-${payload.area}-${payload.kandangName}-${payload.cycleStart}_sd_${payload.cycleEnd}.pdf`; await exportElementToPdf(printRef.current, filename); }, [payload, isEndCycle]); const summary = useMemo( () => (payload ? buildReportSummary(payload) : null), [payload] ); if (!open) return null; const title = isEndCycle ? 'Laporan Akhir Siklus' : 'Laporan Harian'; const cycleLabel = selectedCycle ? `Siklus ${formatDateRange(selectedCycle.start_date, selectedCycle.end_date)}` : ''; const countingTotal = countingTotals(payload?.counting || []); const feedInTotal = feedInTotals(payload?.feedIn || []); const feedUseTotal = feedUseTotals(payload?.feedUse || []); const feedOutTotal = feedOutTotals(payload?.feedOut || []); const endDateLabel = selectedCycle ? formatDateId(selectedCycle.start_date) : ''; const dataDates = selectedCycle && payload ? `(${endDateLabel} - ${formatDateId(payload.counting[payload.counting.length - 1]?.date ?? selectedCycle.start_date)})` : ''; const cycleRange = selectedCycle ? formatDateRange(selectedCycle.start_date, selectedCycle.end_date) : ''; const printedDate = new Date().toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric', }); return (

{title}

{selectedKandang?.kandang_name ?? 'Pilih kandang'} · {cycleLabel} · Hari ke-0 s/d ke- {effectiveUpToDay}

{!isEndCycle && ( <> )}
{error && (
{error}
)}
{!payload ? (

Tekan Generate untuk membuat laporan dari data siklus saat ini.

) : ( <>
{title}
Lokasi: {payload.area} — {payload.kandangName}
Periode: Hari ke-0 s/d ke-{payload.upToDay} {dataDates}
Siklus: {cycleRange}
Berat DOC:{' '} {payload.docWeight == null || !Number.isFinite(payload.docWeight) ? '—' : `${formatNumber(payload.docWeight, 1, 0, true)} gram`}
Dicetak: {printedDate}
{summary && (
{summary.metrics.map((metric) => { const formattedValue = formatSummaryValue(metric); const isHighlight = metric.key === 'fcr' || metric.key === 'eef'; const columnPlacement = isHighlight ? ({ gridColumn: metric.key === 'fcr' ? 2 : 3, marginTop: 12, } as const) : {}; return (
{metric.label}
{formattedValue} {metric.unit && formattedValue !== '—' ? ( {metric.unit} ) : null}
); })}
)} Tanggal Umur (hari) Stok awal Mortalitas Panen Berat panen Stok akhir IoT Akurasi % {payload.counting.map((row) => ( {formatDateId(row.date)} {row.day} {cell(row.countBefore)} {cell(row.mortality)} {cell(row.harvest)} {cell(row.harvestWeight)} {cell(row.countAfter)} {cell(row.iotCount)} {accuracyCell(row.accuracy)} ))} {countingTotal && ( {countingTotal.label} — {dashCell(countingTotal.countBefore)} {cell(countingTotal.mortality)} {cell(countingTotal.harvest)} {cell(countingTotal.harvestWeight)} {dashCell(countingTotal.countAfter)} {dashCell(countingTotal.iotCount)} {countingTotal.accuracy == null ? '-' : accuracyCell(countingTotal.accuracy)} )} Tanggal Umur (hari) Rata-rata manual Rata-rata IoT Keseragaman Selisih Akurasi % {payload.weight.map((row) => ( {formatDateId(row.date)} {row.day} {cell(row.avgManual)} {cell(row.avgIot)} {accuracyCell(row.uniformity)} {cell(row.difference)} {accuracyCell(row.accuracy)} ))} Tanggal Manual masuk IoT masuk Akurasi % {payload.feedIn.map((row) => ( {formatDateId(row.date)} {cell(row.manualIn)} {cell(row.iotIn)} {accuracyCell(row.accuracy)} ))} {feedInTotal && ( {feedInTotal.label} {cell(feedInTotal.manualIn)} {cell(feedInTotal.iotIn)} {accuracyCell(feedInTotal.accuracy)} )} Tanggal Umur (hari) Manual dituang IoT dituang Akurasi % {payload.feedUse.map((row) => ( {formatDateId(row.date)} {row.day} {cell(row.manualUse)} {cell(row.iotUse)} {accuracyCell(row.accuracy)} ))} {feedUseTotal && ( {feedUseTotal.label} — {cell(feedUseTotal.manualUse)} {cell(feedUseTotal.iotUse)} {accuracyCell(feedUseTotal.accuracy)} )} Tanggal Manual keluar IoT keluar Akurasi % {payload.feedOut.map((row) => ( {formatDateId(row.date)} {cell(row.manualOut)} {cell(row.iotOut)} {accuracyCell(row.accuracy)} ))} {feedOutTotal && ( {feedOutTotal.label} {cell(feedOutTotal.manualOut)} {cell(feedOutTotal.iotOut)} {accuracyCell(feedOutTotal.accuracy)} )} {isEndCycle && ( <> Umur Total Pakan Bobot IoT Ayam Hidup Total Bobot Panen FCR {payload.fcr.map((row) => ( Hari {row.day} {qtyCell(row.feedTotal, 0, ' karung')} {qtyCell(row.iotWeight, 2, row.iotWeight != null ? ' g' : '')} {qtyCell(row.chickenLife, 0, ' ekor')} {harvestKgCell(row.harvestWeightTotal)} {ratioCell(row.iotFcr)} ))} Umur Persentase Ayam Hidup BW Rata-rata Panen Umur Rata-rata Panen FCR EEF {payload.eef.map((row) => ( Hari {row.day} {qtyCell(row.chickenLifePercentage, 3, '%')} {qtyCell( row.iotAverageHarvestBw, 2, row.iotAverageHarvestBw != null ? ' g' : '' )} {qtyCell(row.iotAverageDayWeight, 2)} {ratioCell(row.iotFcr)} {ratioCell(row.iotEef)} ))} )} )}
); }; const ReportTable: React.FC<{ title: string; children: React.ReactNode; divided?: boolean; }> = ({ title, children, divided = false }) => (

{title}

{children}
); const Th: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ); const Td: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ); const TotalRow: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ); export default ReportPreviewOverlay;