637 lines
25 KiB
TypeScript
637 lines
25 KiB
TypeScript
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 {
|
|
cycleLastDayIndex,
|
|
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<Props> = ({ open, reportType, onClose }) => {
|
|
const { selectedCycle, selectedKandang, selectedSite } = useFarm();
|
|
const isEndCycle = reportType === 'end_cycle';
|
|
const [draftDay, setDraftDay] = useState(0);
|
|
const [payload, setPayload] = useState<DashboardReportPayload | null>(null);
|
|
const [generating, setGenerating] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const printRef = useRef<HTMLDivElement>(null);
|
|
|
|
const latestDay = selectedCycle ? Math.max(0, selectedCycle.current_day) : 0;
|
|
const endCycleDay = selectedCycle
|
|
? Math.min(selectedCycle.total_days, cycleLastDayIndex(selectedCycle))
|
|
: 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 (
|
|
<div className="fixed inset-0 z-50 bg-slate-900/60" role="dialog" aria-modal="true">
|
|
<div className="flex h-full w-full flex-col bg-slate-50">
|
|
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-4 py-3 sm:px-6">
|
|
<div>
|
|
<h2 id="report-preview-title" className="text-lg font-semibold text-slate-900">
|
|
{title}
|
|
</h2>
|
|
<p className="text-xs text-slate-500">
|
|
{selectedKandang?.kandang_name ?? 'Pilih kandang'} · {cycleLabel} · Hari ke-0 s/d ke-
|
|
{effectiveUpToDay}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{!isEndCycle && (
|
|
<>
|
|
<label className="sr-only" htmlFor="report-day">
|
|
Hari siklus
|
|
</label>
|
|
<select
|
|
id="report-day"
|
|
aria-label="Hari siklus"
|
|
value={draftDay}
|
|
onChange={(event) => setDraftDay(Number(event.target.value))}
|
|
className="rounded-lg border border-slate-200 bg-white px-2 py-1.5 text-xs font-semibold text-slate-700 shadow-sm"
|
|
>
|
|
{dayOptions.map((day) => (
|
|
<option key={day} value={day}>
|
|
Hari {day}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => void generate()}
|
|
disabled={!selectedCycle || !selectedKandang || generating}
|
|
className="rounded-lg bg-slate-900 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{generating ? 'Memproses…' : payload ? 'Perbarui' : 'Generate'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => payload && void downloadPdf()}
|
|
disabled={!payload}
|
|
className="rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
PDF
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => payload && void downloadReportExcel(payload)}
|
|
disabled={!payload}
|
|
className="rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
Excel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="rounded-lg border border-slate-200 px-3 py-1.5 text-xs font-medium text-slate-600 hover:bg-slate-50"
|
|
aria-label="Tutup"
|
|
>
|
|
<i className="fa-solid fa-xmark" />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex-1 overflow-auto px-4 py-4 sm:px-6">
|
|
{error && (
|
|
<div className="mb-4">
|
|
<Banner kind="error">{error}</Banner>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
id="report-snapshot-print"
|
|
ref={printRef}
|
|
className="rounded-xl bg-white p-4 shadow-sm"
|
|
>
|
|
{!payload ? (
|
|
<p className="py-8 text-center text-sm text-slate-500">
|
|
Tekan <strong>Generate</strong> untuk membuat laporan dari data siklus saat ini.
|
|
</p>
|
|
) : (
|
|
<>
|
|
<div className="mb-6 border-b-2 border-purple-700 pb-4">
|
|
<div style={{ fontSize: 17, fontWeight: 700, color: '#111827' }}>{title}</div>
|
|
<div style={{ fontSize: 13, color: '#374151', marginTop: 5 }}>
|
|
<strong>Lokasi:</strong> {payload.area} — {payload.kandangName}
|
|
</div>
|
|
<div style={{ fontSize: 13, color: '#374151', marginTop: 2 }}>
|
|
<strong>Periode:</strong> Hari ke-0 s/d ke-{payload.upToDay} {dataDates}
|
|
</div>
|
|
<div style={{ fontSize: 12, color: '#6b7280', marginTop: 2 }}>
|
|
<strong>Siklus:</strong> {cycleRange}
|
|
</div>
|
|
<div style={{ fontSize: 12, color: '#6b7280', marginTop: 2 }}>
|
|
<strong>Berat DOC:</strong>{' '}
|
|
{payload.docWeight == null || !Number.isFinite(payload.docWeight)
|
|
? '—'
|
|
: `${formatNumber(payload.docWeight, 1, 0, true)} gram`}
|
|
</div>
|
|
<div style={{ fontSize: 12, color: '#6b7280', marginTop: 2 }}>
|
|
Dicetak: {printedDate}
|
|
</div>
|
|
</div>
|
|
|
|
{summary && (
|
|
<div
|
|
data-pdf-block
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
|
|
gap: 8,
|
|
marginBottom: 24,
|
|
}}
|
|
>
|
|
{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 (
|
|
<div
|
|
key={metric.key}
|
|
style={
|
|
isHighlight
|
|
? {
|
|
border: '2px solid #5b21b6',
|
|
borderRadius: 10,
|
|
padding: '12px 12px 14px',
|
|
background: 'linear-gradient(180deg, #6d28d0 0%, #5b21b6 100%)',
|
|
textAlign: 'center',
|
|
boxShadow: '0 2px 0 #4c1d95',
|
|
...columnPlacement,
|
|
}
|
|
: {
|
|
border: '1px solid #e9d5ff',
|
|
borderTop: '3px solid #7e22ce',
|
|
borderRadius: 8,
|
|
padding: '8px 10px',
|
|
background: '#faf5ff',
|
|
...columnPlacement,
|
|
}
|
|
}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: isHighlight ? 11 : 10,
|
|
color: isHighlight ? '#e9d5ff' : '#6b7280',
|
|
fontWeight: 700,
|
|
letterSpacing: isHighlight ? 0.6 : 0,
|
|
textTransform: isHighlight ? 'uppercase' : 'none',
|
|
}}
|
|
>
|
|
{metric.label}
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: isHighlight ? 26 : 16,
|
|
color: isHighlight ? '#ffffff' : '#111827',
|
|
fontWeight: 800,
|
|
marginTop: isHighlight ? 6 : 2,
|
|
lineHeight: 1.1,
|
|
}}
|
|
>
|
|
{formattedValue}
|
|
{metric.unit && formattedValue !== '—' ? (
|
|
<span
|
|
style={{
|
|
fontSize: 10,
|
|
fontWeight: 500,
|
|
color: '#6b7280',
|
|
marginLeft: 4,
|
|
}}
|
|
>
|
|
{metric.unit}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<ReportTable title="Hitung Ayam harian">
|
|
<thead>
|
|
<tr>
|
|
<Th>Tanggal</Th>
|
|
<Th>Umur (hari)</Th>
|
|
<Th>Stok awal</Th>
|
|
<Th>Mortalitas</Th>
|
|
<Th>Panen</Th>
|
|
<Th>Berat panen</Th>
|
|
<Th>Stok akhir</Th>
|
|
<Th>IoT</Th>
|
|
<Th>Akurasi %</Th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{payload.counting.map((row) => (
|
|
<tr key={`c-${row.day}`} className="border-t border-slate-100" data-pdf-block>
|
|
<Td>{formatDateId(row.date)}</Td>
|
|
<Td>{row.day}</Td>
|
|
<Td>{cell(row.countBefore)}</Td>
|
|
<Td>{cell(row.mortality)}</Td>
|
|
<Td>{cell(row.harvest)}</Td>
|
|
<Td>{cell(row.harvestWeight)}</Td>
|
|
<Td>{cell(row.countAfter)}</Td>
|
|
<Td>{cell(row.iotCount)}</Td>
|
|
<Td>{accuracyCell(row.accuracy)}</Td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
{countingTotal && (
|
|
<tfoot>
|
|
<TotalRow>
|
|
<Td>{countingTotal.label}</Td>
|
|
<Td>—</Td>
|
|
<Td>{dashCell(countingTotal.countBefore)}</Td>
|
|
<Td>{cell(countingTotal.mortality)}</Td>
|
|
<Td>{cell(countingTotal.harvest)}</Td>
|
|
<Td>{cell(countingTotal.harvestWeight)}</Td>
|
|
<Td>{dashCell(countingTotal.countAfter)}</Td>
|
|
<Td>{dashCell(countingTotal.iotCount)}</Td>
|
|
<Td>
|
|
{countingTotal.accuracy == null
|
|
? '-'
|
|
: accuracyCell(countingTotal.accuracy)}
|
|
</Td>
|
|
</TotalRow>
|
|
</tfoot>
|
|
)}
|
|
</ReportTable>
|
|
|
|
<ReportTable title="Berat Ayam harian" divided>
|
|
<thead>
|
|
<tr>
|
|
<Th>Tanggal</Th>
|
|
<Th>Umur (hari)</Th>
|
|
<Th>Rata-rata manual</Th>
|
|
<Th>Rata-rata IoT</Th>
|
|
<Th>Keseragaman</Th>
|
|
<Th>Selisih</Th>
|
|
<Th>Akurasi %</Th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{payload.weight.map((row) => (
|
|
<tr key={`w-${row.day}`} className="border-t border-slate-100" data-pdf-block>
|
|
<Td>{formatDateId(row.date)}</Td>
|
|
<Td>{row.day}</Td>
|
|
<Td>{cell(row.avgManual)}</Td>
|
|
<Td>{cell(row.avgIot)}</Td>
|
|
<Td>{accuracyCell(row.uniformity)}</Td>
|
|
<Td>{cell(row.difference)}</Td>
|
|
<Td>{accuracyCell(row.accuracy)}</Td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</ReportTable>
|
|
|
|
<ReportTable title="Karung masuk" divided>
|
|
<thead>
|
|
<tr>
|
|
<Th>Tanggal</Th>
|
|
<Th>Manual masuk</Th>
|
|
<Th>IoT masuk</Th>
|
|
<Th>Akurasi %</Th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{payload.feedIn.map((row) => (
|
|
<tr
|
|
key={`in-${row.date}`}
|
|
className="border-t border-slate-100"
|
|
data-pdf-block
|
|
>
|
|
<Td>{formatDateId(row.date)}</Td>
|
|
<Td>{cell(row.manualIn)}</Td>
|
|
<Td>{cell(row.iotIn)}</Td>
|
|
<Td>{accuracyCell(row.accuracy)}</Td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
{feedInTotal && (
|
|
<tfoot>
|
|
<TotalRow>
|
|
<Td>{feedInTotal.label}</Td>
|
|
<Td>{cell(feedInTotal.manualIn)}</Td>
|
|
<Td>{cell(feedInTotal.iotIn)}</Td>
|
|
<Td>{accuracyCell(feedInTotal.accuracy)}</Td>
|
|
</TotalRow>
|
|
</tfoot>
|
|
)}
|
|
</ReportTable>
|
|
|
|
<ReportTable title="Karung dituang" divided>
|
|
<thead>
|
|
<tr>
|
|
<Th>Tanggal</Th>
|
|
<Th>Umur (hari)</Th>
|
|
<Th>Manual dituang</Th>
|
|
<Th>IoT dituang</Th>
|
|
<Th>Akurasi %</Th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{payload.feedUse.map((row) => (
|
|
<tr
|
|
key={`use-${row.day}-${row.date}`}
|
|
className="border-t border-slate-100"
|
|
data-pdf-block
|
|
>
|
|
<Td>{formatDateId(row.date)}</Td>
|
|
<Td>{row.day}</Td>
|
|
<Td>{cell(row.manualUse)}</Td>
|
|
<Td>{cell(row.iotUse)}</Td>
|
|
<Td>{accuracyCell(row.accuracy)}</Td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
{feedUseTotal && (
|
|
<tfoot>
|
|
<TotalRow>
|
|
<Td>{feedUseTotal.label}</Td>
|
|
<Td>—</Td>
|
|
<Td>{cell(feedUseTotal.manualUse)}</Td>
|
|
<Td>{cell(feedUseTotal.iotUse)}</Td>
|
|
<Td>{accuracyCell(feedUseTotal.accuracy)}</Td>
|
|
</TotalRow>
|
|
</tfoot>
|
|
)}
|
|
</ReportTable>
|
|
|
|
<ReportTable title="Karung keluar" divided>
|
|
<thead>
|
|
<tr>
|
|
<Th>Tanggal</Th>
|
|
<Th>Manual keluar</Th>
|
|
<Th>IoT keluar</Th>
|
|
<Th>Akurasi %</Th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{payload.feedOut.map((row) => (
|
|
<tr
|
|
key={`out-${row.date}`}
|
|
className="border-t border-slate-100"
|
|
data-pdf-block
|
|
>
|
|
<Td>{formatDateId(row.date)}</Td>
|
|
<Td>{cell(row.manualOut)}</Td>
|
|
<Td>{cell(row.iotOut)}</Td>
|
|
<Td>{accuracyCell(row.accuracy)}</Td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
{feedOutTotal && (
|
|
<tfoot>
|
|
<TotalRow>
|
|
<Td>{feedOutTotal.label}</Td>
|
|
<Td>{cell(feedOutTotal.manualOut)}</Td>
|
|
<Td>{cell(feedOutTotal.iotOut)}</Td>
|
|
<Td>{accuracyCell(feedOutTotal.accuracy)}</Td>
|
|
</TotalRow>
|
|
</tfoot>
|
|
)}
|
|
</ReportTable>
|
|
|
|
{isEndCycle && (
|
|
<>
|
|
<ReportTable title="Data Pendukung FCR" divided>
|
|
<thead>
|
|
<tr>
|
|
<Th>Umur</Th>
|
|
<Th>Total Pakan</Th>
|
|
<Th>Bobot IoT</Th>
|
|
<Th>Ayam Hidup</Th>
|
|
<Th>Total Bobot Panen</Th>
|
|
<Th>FCR</Th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{payload.fcr.map((row) => (
|
|
<tr
|
|
key={`fcr-${row.day}`}
|
|
className="border-t border-slate-100"
|
|
data-pdf-block
|
|
>
|
|
<Td>Hari {row.day}</Td>
|
|
<Td>{qtyCell(row.feedTotal, 0, ' karung')}</Td>
|
|
<Td>{qtyCell(row.iotWeight, 2, row.iotWeight != null ? ' g' : '')}</Td>
|
|
<Td>{qtyCell(row.chickenLife, 0, ' ekor')}</Td>
|
|
<Td>{harvestKgCell(row.harvestWeightTotal)}</Td>
|
|
<Td>{ratioCell(row.iotFcr)}</Td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</ReportTable>
|
|
|
|
<ReportTable title="Data Pendukung EEF" divided>
|
|
<thead>
|
|
<tr>
|
|
<Th>Umur</Th>
|
|
<Th>Persentase Ayam Hidup</Th>
|
|
<Th>BW Rata-rata Panen</Th>
|
|
<Th>Umur Rata-rata Panen</Th>
|
|
<Th>FCR</Th>
|
|
<Th>EEF</Th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{payload.eef.map((row) => (
|
|
<tr
|
|
key={`eef-${row.day}`}
|
|
className="border-t border-slate-100"
|
|
data-pdf-block
|
|
>
|
|
<Td>Hari {row.day}</Td>
|
|
<Td>{qtyCell(row.chickenLifePercentage, 3, '%')}</Td>
|
|
<Td>
|
|
{qtyCell(
|
|
row.iotAverageHarvestBw,
|
|
2,
|
|
row.iotAverageHarvestBw != null ? ' g' : ''
|
|
)}
|
|
</Td>
|
|
<Td>{qtyCell(row.iotAverageDayWeight, 2)}</Td>
|
|
<Td>{ratioCell(row.iotFcr)}</Td>
|
|
<Td>{ratioCell(row.iotEef)}</Td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</ReportTable>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ReportTable: React.FC<{
|
|
title: string;
|
|
children: React.ReactNode;
|
|
divided?: boolean;
|
|
}> = ({ title, children, divided = false }) => (
|
|
<section
|
|
data-pdf-block="true"
|
|
className={divided ? 'mt-10 border-t-2 border-slate-400 pt-10' : undefined}
|
|
>
|
|
<h3 className="mb-2 text-sm font-semibold text-slate-800">{title}</h3>
|
|
<div className="overflow-x-auto rounded-lg border border-slate-200">
|
|
<table className="min-w-full text-left text-xs text-slate-700">{children}</table>
|
|
</div>
|
|
</section>
|
|
);
|
|
|
|
const Th: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
<th className="bg-slate-50 px-3 py-2 font-semibold text-slate-600">{children}</th>
|
|
);
|
|
|
|
const Td: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
<td className="px-3 py-2 tabular-nums">{children}</td>
|
|
);
|
|
|
|
const TotalRow: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
<tr className="border-t-2 border-slate-200 bg-slate-50 font-semibold" data-pdf-block>
|
|
{children}
|
|
</tr>
|
|
);
|
|
|
|
export default ReportPreviewOverlay;
|