563 lines
32 KiB
TypeScript
563 lines
32 KiB
TypeScript
'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>
|
||
)}
|
||
</>
|
||
)
|
||
}
|