TKK_E32231405/app/dashboard/kelola-data/CetakInstanModal.tsx

563 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useRef, useMemo, useEffect } from 'react'
import { FileDown, FolderOpen, X, Loader2, CheckCircle2, ChevronDown } from 'lucide-react'
import { supabase } from '@/lib/supabase'
import { showSwal } from '@/lib/swal'
import {
AreaChart, Area,
XAxis, YAxis, CartesianGrid,
ResponsiveContainer,
} from 'recharts'
// ─── TYPES ────────────────────────────────────────────────────────
interface HasilItem {
id: number
id_balita: number
tinggi_badan: number | null
berat_badan: number | null
z_score: number | null
status_stunting: boolean | null
pesan_ai: string | null
tanggal_upload: string | null
nama_posyandu: string | null
}
interface PenggunaData {
id: number
nama_orang_tua: string
alamat: string | null
nama_anak: string
jenis_kelamin: string | null
tanggal_lahir: string | null
}
const MONTHS = [
{ num: 1, name: 'Januari' }, { num: 2, name: 'Februari' },
{ num: 3, name: 'Maret' }, { num: 4, name: 'April' },
{ num: 5, name: 'Mei' }, { num: 6, name: 'Juni' },
{ num: 7, name: 'Juli' }, { num: 8, name: 'Agustus' },
{ num: 9, name: 'September' }, { num: 10, name: 'Oktober' },
{ num: 11, name: 'November' }, { num: 12, name: 'Desember' },
]
const START_YEAR = 2026
const currentYear = new Date().getFullYear()
const YEARS = Array.from(
{ length: Math.max(currentYear, START_YEAR) - START_YEAR + 1 },
(_, i) => START_YEAR + i,
)
type Step = 'config' | 'generating' | 'done' | 'error'
// ─── HELPERS ──────────────────────────────────────────────────────
function formatTgl(d: string | null, style: 'long' | 'short' = 'long') {
if (!d) return '-'
return new Date(d).toLocaleDateString('id-ID', {
day: 'numeric',
month: style === 'long' ? 'long' : 'short',
year: 'numeric',
})
}
function build5MonthData(allData: HasilItem[], rowDate: Date) {
const slots = []
for (let i = 4; i >= 0; i--) {
const d = new Date(rowDate.getFullYear(), rowDate.getMonth() - i, 1)
slots.push({ year: d.getFullYear(), month: d.getMonth() + 1 })
}
return slots.map(slot => {
const match = allData.find(item => {
if (!item.tanggal_upload) return false
const id = new Date(item.tanggal_upload)
return id.getFullYear() === slot.year && id.getMonth() + 1 === slot.month && id <= rowDate
})
const label = new Date(slot.year, slot.month - 1, 1).toLocaleDateString('id-ID', { month: 'short', year: '2-digit' })
return {
label,
tinggi: match?.tinggi_badan ?? null,
berat: match?.berat_badan ?? null,
zscore: match?.z_score ?? null,
}
})
}
export function CetakInstanModal() {
const [open, setOpen] = useState(false)
const [year, setYear] = useState(START_YEAR)
const [month, setMonth] = useState(new Date().getMonth() + 1)
const [dirHandle, setDirHandle] = useState<FileSystemDirectoryHandle | null>(null)
const [step, setStep] = useState<Step>('config')
const [progress, setProgress] = useState({ current: 0, total: 0, name: '', mama: '' })
const [elapsed, setElapsed] = useState(0)
const [errorMsg, setErrorMsg] = useState('')
// Template state for batch processing
const [activePrintData, setActivePrintData] = useState<{
pengguna: PenggunaData
row: HasilItem
allHasil: HasilItem[]
} | null>(null)
const templateRef = useRef<HTMLDivElement>(null)
const monthName = MONTHS.find(m => m.num === month)?.name ?? 'Bulan'
const folderName = `${monthName.toLowerCase()}_${year}`
const supportsFileSys = typeof window !== 'undefined' && 'showDirectoryPicker' in window
const pickDir = async () => {
try {
const handle = await (window as any).showDirectoryPicker({ mode: 'readwrite' })
setDirHandle(handle)
} catch { /* user cancelled */ }
}
const reset = () => {
setStep('config')
setProgress({ current: 0, total: 0, name: '', mama: '' })
setElapsed(0)
setErrorMsg('')
setActivePrintData(null)
}
const handleClose = () => {
setOpen(false)
setTimeout(reset, 300)
}
const handleGenerate = async () => {
setStep('generating')
const startTime = Date.now()
const timer = setInterval(() => setElapsed(Math.floor((Date.now() - startTime) / 1000)), 500)
try {
const { default: html2canvas } = await import('html2canvas')
const { default: jsPDF } = await import('jspdf')
// 1. Fetch data
const { data: balitaList, error: eB } = await supabase
.from('akun_balita')
.select('*')
.order('nama_orang_tua', { ascending: true })
if (eB) throw new Error(eB.message)
const fromDate = new Date(year - 1, month - 2, 1).toISOString().split('T')[0]
const toDate = new Date(year, month - 1, 31).toISOString().split('T')[0]
const { data: hasilAll, error: eH } = await supabase
.from('hasil_stunting_balita')
.select('*')
.gte('tanggal_upload', fromDate)
.lte('tanggal_upload', toDate)
.order('tanggal_upload', { ascending: true })
if (eH) throw new Error(eH.message)
const targets = (balitaList ?? []).filter(b =>
(hasilAll ?? []).some(h => {
if (h.id_balita !== b.id || !h.tanggal_upload) return false
const d = new Date(h.tanggal_upload)
return d.getFullYear() === year && d.getMonth() + 1 === month
})
)
if (targets.length === 0) {
setErrorMsg(`Tidak ada data balita untuk ${monthName} ${year}`)
setStep('error')
clearInterval(timer)
return
}
setProgress({ current: 0, total: targets.length, name: '', mama: '' })
// 2. Prepare folder
let folderHandle: FileSystemDirectoryHandle | null = null
if (dirHandle) {
folderHandle = await dirHandle.getDirectoryHandle(folderName, { create: true })
}
// 3. Generation Loop
for (let i = 0; i < targets.length; i++) {
const b = targets[i] as PenggunaData
const balitaHasil = (hasilAll ?? []).filter(h => h.id_balita === b.id) as HasilItem[]
const rowForMonth = balitaHasil.find(h => {
if (!h.tanggal_upload) return false
const d = new Date(h.tanggal_upload)
return d.getFullYear() === year && d.getMonth() + 1 === month
})
if (!rowForMonth) continue
setProgress({ current: i + 1, total: targets.length, name: b.nama_anak, mama: b.nama_orang_tua })
// --- Update template and wait for render ---
setActivePrintData({ pengguna: b, row: rowForMonth, allHasil: balitaHasil })
// Give React and Recharts some time to finish rendering the hidden template
await new Promise(r => setTimeout(r, 1000)) // 1s buffer for stable DOM & Recharts
if (!templateRef.current) continue
// --- Capture ---
const canvas = await html2canvas(templateRef.current, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
logging: false,
})
const imgData = canvas.toDataURL('image/jpeg', 0.95)
// Safety check: ensure imgData is a valid Data URI
if (!imgData || !imgData.startsWith('data:image/')) {
console.error('Invalid image data generated for', b.nama_anak)
continue
}
const pdf = new jsPDF('p', 'mm', 'a4')
const pageW = pdf.internal.pageSize.getWidth()
const pageH = pdf.internal.pageSize.getHeight()
const imgH = (canvas.height * pageW) / canvas.width
if (imgH <= pageH) {
pdf.addImage(imgData, 'JPEG', 0, 0, pageW, imgH)
} else {
let yPos = 0
const sliceH = canvas.width * (pageH / pageW)
while (yPos < canvas.height) {
const sliceCanvas = document.createElement('canvas')
sliceCanvas.width = canvas.width
sliceCanvas.height = Math.min(sliceH, canvas.height - yPos)
const ctx = sliceCanvas.getContext('2d')!
ctx.drawImage(canvas, 0, -yPos)
if (yPos > 0) pdf.addPage()
pdf.addImage(sliceCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', 0, 0, pageW, pageH)
yPos += sliceH
}
}
// --- Save ---
const fileName = `Laporan_${b.nama_anak.replace(/\s+/g, '_')}.pdf`
const blob = pdf.output('blob')
if (folderHandle) {
const fh = await folderHandle.getFileHandle(fileName, { create: true })
const writable = await fh.createWritable()
await writable.write(blob)
await writable.close()
} else {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
a.click()
URL.revokeObjectURL(url)
await new Promise(r => setTimeout(r, 200))
}
}
clearInterval(timer)
setStep('done')
await showSwal.success('Selesai!', `Berhasil mencetak ${targets.length} file PDF.`)
handleClose()
} catch (err: any) {
clearInterval(timer)
setErrorMsg(err?.message ?? 'Terjadi kesalahan saat mencetak.')
setStep('error')
showSwal.error('Gagal!', err?.message ?? 'Terjadi kesalahan saat mencetak.')
} finally {
setActivePrintData(null)
}
}
const pct = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0
const estRemaining = progress.current > 0
? Math.round((elapsed / progress.current) * (progress.total - progress.current))
: null
// ── Template helper variables ──────────────────────────────────
const chartData = useMemo(() => {
if (!activePrintData) return []
const rowDate = activePrintData.row.tanggal_upload ? new Date(activePrintData.row.tanggal_upload) : new Date()
return build5MonthData(activePrintData.allHasil, rowDate)
}, [activePrintData])
const isStunting = activePrintData?.row.status_stunting === true
const tanggalCetak = new Date().toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' })
return (
<>
{/* Trigger button */}
<button
onClick={() => { setOpen(true); reset() }}
className="flex items-center gap-2 px-4 py-2.5 bg-black text-white text-sm font-bold rounded-xl hover:bg-gray-800 transition-all shadow-[4px_4px_0px_0px_rgba(0,0,0,0.25)] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,0.2)] hover:translate-x-[1px] hover:translate-y-[1px]"
>
<FileDown className="w-4 h-4" />
Cetak Data Instan
</button>
{/* Backdrop + Modal */}
{open && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ backgroundColor: 'rgba(0,0,0,0.6)' }}
onClick={step === 'config' ? handleClose : undefined}
>
<div
className="bg-white rounded-2xl border-2 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] w-full max-w-md overflow-hidden"
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-5 border-b-2 border-black bg-black text-white">
<div className="flex items-center gap-3">
<FileDown className="w-5 h-5" />
<div>
<p className="font-black text-lg leading-none">Cetak Data Instan</p>
<p className="text-[10px] text-gray-400 mt-0.5 uppercase tracking-widest">
Generate PDF semua balita per periode
</p>
</div>
</div>
{step !== 'generating' && (
<button onClick={handleClose} className="p-1.5 rounded-full hover:bg-white/10 transition-colors">
<X className="w-4 h-4" />
</button>
)}
</div>
{/* ── Steps: Config, Generating, Done, Error ── */}
{step === 'config' && (
<div className="p-6 flex flex-col gap-6">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-gray-500 mb-3">Pilih Periode</p>
<div className="grid grid-cols-2 gap-3">
<div className="relative">
<label className="text-[10px] font-bold uppercase tracking-widest text-gray-400 mb-1 block">Tahun</label>
<select value={year} onChange={e => setYear(Number(e.target.value))} className="w-full appearance-none border-2 border-gray-200 focus:border-black rounded-lg pl-3 pr-8 py-2.5 text-sm font-bold bg-white focus:outline-none transition-colors cursor-pointer">
{YEARS.map(y => <option key={y} value={y}>{y}</option>)}
</select>
<ChevronDown className="absolute right-3 bottom-2.5 w-4 h-4 text-gray-400 pointer-events-none" />
</div>
<div className="relative">
<label className="text-[10px] font-bold uppercase tracking-widest text-gray-400 mb-1 block">Bulan</label>
<select value={month} onChange={e => setMonth(Number(e.target.value))} className="w-full appearance-none border-2 border-gray-200 focus:border-black rounded-lg pl-3 pr-8 py-2.5 text-sm font-bold bg-white focus:outline-none transition-colors cursor-pointer">
{MONTHS.map(m => <option key={m.num} value={m.num}>{m.name}</option>)}
</select>
<ChevronDown className="absolute right-3 bottom-2.5 w-4 h-4 text-gray-400 pointer-events-none" />
</div>
</div>
</div>
<div>
<p className="text-xs font-bold uppercase tracking-widest text-gray-500 mb-3">Lokasi Penyimpanan</p>
{supportsFileSys ? (
<div className="flex flex-col gap-2">
<button onClick={pickDir} className="flex items-center gap-2 px-4 py-2.5 border-2 border-dashed border-gray-300 hover:border-black rounded-xl text-sm font-semibold text-gray-600 hover:text-black transition-all">
<FolderOpen className="w-4 h-4" />
{dirHandle ? dirHandle.name : 'Pilih folder tujuan...'}
</button>
{dirHandle && <div className="flex items-center gap-2 text-xs text-emerald-600 font-semibold"><CheckCircle2 className="w-3.5 h-3.5" />Folder dibuat: <span className="font-black">{folderName}</span></div>}
</div>
) : (
<div className="px-4 py-3 bg-amber-50 border border-amber-200 rounded-xl text-xs text-amber-700">Browser tidak mendukung pemilihan direktori. PDF akan diunduh satu per satu.</div>
)}
</div>
<button onClick={handleGenerate} className="w-full py-3 bg-black text-white font-black text-sm rounded-xl hover:bg-gray-800 transition-all flex items-center justify-center gap-2">
<FileDown className="w-4 h-4" />
Mulai Cetak PDF
</button>
</div>
)}
{step === 'generating' && (
<div className="p-6 flex flex-col gap-5">
<div className="flex items-center gap-3">
<Loader2 className="w-5 h-5 text-black animate-spin flex-shrink-0" />
<div>
<p className="font-bold text-base">Sedang mencetak PDF...</p>
<p className="text-xs text-gray-400">Harap tunggu, jangan tutup halaman ini.</p>
</div>
</div>
<div>
<div className="flex justify-between text-xs font-bold mb-1.5">
<span>{progress.current} / {progress.total} balita</span>
<span>{pct}%</span>
</div>
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden border border-gray-200">
<div className="h-full bg-black rounded-full transition-all duration-500" style={{ width: `${pct}%` }} />
</div>
</div>
{progress.name && (
<div className="px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl animate-pulse">
<p className="font-black text-sm">{progress.name}</p>
<p className="text-xs text-gray-500">Ibu: {progress.mama}</p>
</div>
)}
<div className="grid grid-cols-2 gap-3">
<div className="text-center px-3 py-3 bg-gray-50 border border-gray-200 rounded-xl">
<p className="text-2xl font-black font-mono">{String(Math.floor(elapsed / 60)).padStart(2, '0')}:{String(elapsed % 60).padStart(2, '0')}</p>
</div>
<div className="text-center px-3 py-3 bg-gray-50 border border-gray-200 rounded-xl">
<p className="text-2xl font-black font-mono">{estRemaining !== null ? `${String(Math.floor(estRemaining / 60)).padStart(2, '0')}:${String(estRemaining % 60).padStart(2, '0')}` : '--:--'}</p>
</div>
</div>
</div>
)}
{step === 'done' && (
<div className="p-6 flex flex-col gap-5 items-center text-center">
<CheckCircle2 className="w-12 h-12 text-emerald-500" />
<p className="font-black text-xl">Selesai! {progress.total} file dicetak.</p>
<button onClick={handleClose} className="px-8 py-2.5 bg-black text-white font-bold rounded-xl hover:bg-gray-800 transition-colors">Tutup</button>
</div>
)}
{step === 'error' && (
<div className="p-6 flex flex-col gap-5 items-center text-center">
<p className="font-black text-xl text-red-700">Gagal</p>
<p className="text-sm text-gray-500">{errorMsg}</p>
<button onClick={reset} className="px-8 py-2.5 bg-black text-white font-bold rounded-xl">Coba Lagi</button>
</div>
)}
</div>
</div>
)}
{/* ─── HIDDEN PDF TEMPLATE (Rich HTML) ─── */}
{activePrintData && (
<div style={{ position: 'fixed', left: 0, top: 0, opacity: 0, pointerEvents: 'none', zIndex: -100, width: 'fit-content' }}>
<div
ref={templateRef}
style={{
width: 794,
backgroundColor: '#ffffff',
fontFamily: 'Arial, Helvetica, sans-serif',
color: '#111111',
padding: '48px 56px',
boxSizing: 'border-box',
}}
>
{/* Header */}
<div style={{ borderBottom: '3px solid #111', paddingBottom: 20, marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 3, textTransform: 'uppercase', color: '#555', marginBottom: 6 }}>Sistem Informasi Posyandu</div>
<div style={{ fontSize: 22, fontWeight: 900, letterSpacing: -0.5 }}>Laporan Pemeriksaan Balita</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: 10, color: '#888' }}>Tanggal Cetak</div>
<div style={{ fontSize: 13, fontWeight: 700 }}>{tanggalCetak}</div>
<div style={{ marginTop: 6, fontSize: 10, color: '#888' }}>Tgl Pemeriksaan</div>
<div style={{ fontSize: 13, fontWeight: 700 }}>{formatTgl(activePrintData.row.tanggal_upload)}</div>
</div>
</div>
{/* Identitas */}
<div style={{ marginBottom: 28 }}>
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', color: '#888', marginBottom: 10 }}>Identitas</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px 32px' }}>
{[
['Nama Ibu / Orang Tua', activePrintData.pengguna.nama_orang_tua],
['Nama Anak', activePrintData.pengguna.nama_anak],
['Alamat', activePrintData.pengguna.alamat ?? '-'],
['Jenis Kelamin', activePrintData.pengguna.jenis_kelamin ?? '-'],
['Tanggal Lahir', formatTgl(activePrintData.pengguna.tanggal_lahir)],
].map(([label, value], i) => (
<div key={i} style={{ borderBottom: '1px solid #eee', paddingBottom: 8 }}>
<div style={{ fontSize: 9, color: '#888', fontWeight: 700, textTransform: 'uppercase' }}>{label}</div>
<div style={{ fontSize: 13, fontWeight: 600 }}>{value}</div>
</div>
))}
</div>
</div>
{/* Charts */}
<div style={{ marginBottom: 28 }}>
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', color: '#888', marginBottom: 10 }}>Grafik Perkembangan (5 Bulan)</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
<div style={{ border: '1.5px solid #dbeafe', borderRadius: 12, padding: '14px 14px 4px', background: '#eff6ff' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: '#1d4ed8', marginBottom: 8 }}>📏 Tinggi Badan (cm)</div>
<ResponsiveContainer width="100%" height={140}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#bfdbfe" />
<XAxis dataKey="label" fontSize={9} axisLine={false} tickLine={false} />
<YAxis fontSize={9} axisLine={false} tickLine={false} />
<Area type="monotone" dataKey="tinggi" stroke="#3b82f6" strokeWidth={2} fill="none" dot={{ r: 4, fill: '#3b82f6', stroke: 'white' }} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
<div style={{ border: '1.5px solid #d1fae5', borderRadius: 12, padding: '14px 14px 4px', background: '#f0fdf4' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: '#059669', marginBottom: 8 }}> Berat Badan (kg)</div>
<ResponsiveContainer width="100%" height={140}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#a7f3d0" />
<XAxis dataKey="label" fontSize={9} axisLine={false} tickLine={false} />
<YAxis fontSize={9} axisLine={false} tickLine={false} />
<Area type="monotone" dataKey="berat" stroke="#10b981" strokeWidth={2} fill="none" dot={{ r: 4, fill: '#10b981', stroke: 'white' }} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* Z-Score PDF Chart */}
<div style={{ marginTop: 20, border: '1.5px solid #f3e8ff', borderRadius: 12, padding: '14px 14px 4px', background: '#faf5ff' }}>
<div style={{ fontSize: 11, fontWeight: 700, color: '#9333ea', marginBottom: 8 }}>📈 Z-Score (SD)</div>
<ResponsiveContainer width="100%" height={100}>
<AreaChart data={chartData} margin={{ top: 4, right: 16, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e9d5ff" />
<XAxis dataKey="label" fontSize={9} axisLine={false} tickLine={false} />
<YAxis fontSize={9} axisLine={false} tickLine={false} />
<Area type="monotone" dataKey="zscore" stroke="#9333ea" strokeWidth={2} fill="none" dot={{ r: 4, fill: '#9333ea', stroke: 'white' }} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* Table */}
<div style={{ marginBottom: 32 }}>
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', color: '#888', marginBottom: 10 }}>Data Pemeriksaan</div>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
<thead>
<tr style={{ background: '#111', color: '#fff' }}>
{['Tinggi', 'Berat', 'Z-Score', 'Status', 'Posyandu', 'Tgl Upload'].map(h => (
<th key={h} style={{ padding: '8px 12px', textAlign: 'left', fontSize: 10 }}>{h}</th>
))}
</tr>
</thead>
<tbody>
<tr style={{ background: '#f9fafb' }}>
<td style={{ padding: '10px 12px', fontWeight: 700 }}>{activePrintData.row.tinggi_badan} cm</td>
<td style={{ padding: '10px 12px', fontWeight: 700 }}>{activePrintData.row.berat_badan} kg</td>
<td style={{ padding: '10px 12px', fontWeight: 700 }}>{activePrintData.row.z_score} SD</td>
<td style={{ padding: '10px 12px' }}>
<span style={{ padding: '2px 8px', borderRadius: 10, fontSize: 10, fontWeight: 700, background: isStunting ? '#fee2e2' : '#dcfce7', color: isStunting ? '#991b1b' : '#166534' }}>
{isStunting ? 'Stunting' : 'Normal'}
</span>
</td>
<td style={{ padding: '10px 12px' }}>{activePrintData.row.nama_posyandu}</td>
<td style={{ padding: '10px 12px' }}>{formatTgl(activePrintData.row.tanggal_upload)}</td>
</tr>
</tbody>
</table>
{activePrintData.row.pesan_ai && (
<div style={{ marginTop: 15, border: '1px solid #fde68a', borderRadius: 8, padding: '12px', background: '#fffbeb' }}>
<div style={{ fontSize: 9, fontWeight: 700, color: '#92400e', marginBottom: 4 }}>PESAN AI</div>
<div style={{ fontSize: 11, color: '#78350f' }}>{activePrintData.row.pesan_ai}</div>
</div>
)}
</div>
{/* Signatures */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 40, marginTop: 40, paddingTop: 20, borderTop: '1px solid #eee' }}>
<div>
<div style={{ fontSize: 10, color: '#888', marginBottom: 40 }}>Mengetahui,</div>
<div style={{ borderTop: '1px solid #333', fontSize: 10, paddingTop: 4 }}>Supervisor</div>
</div>
<div>
<div style={{ fontSize: 10, color: '#888', marginBottom: 40 }}>Petugas Posyandu,</div>
<div style={{ borderTop: '1px solid #333', fontSize: 10, paddingTop: 4 }}>Nama & Tanda Tangan</div>
</div>
</div>
</div>
</div>
)}
</>
)
}