TKK_E32231405/app/user-dashboard/perkembangan/ExportPDFButton.tsx

371 lines
21 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 { useRef, useMemo, useState } from 'react'
import { Printer } from 'lucide-react'
import {
AreaChart, Area,
XAxis, YAxis, CartesianGrid,
ResponsiveContainer,
} from 'recharts'
import { showSwal } from '@/lib/swal'
interface HasilItem {
id: 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 Pengguna {
nama_orang_tua: string
alamat: string | null
nama_anak: string
jenis_kelamin: string | null
tanggal_lahir: string | null
username?: string | null
password?: string | null
}
interface Props {
row: HasilItem
allData: HasilItem[]
pengguna: Pengguna
}
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 ExportPDFButton({ row, allData, pengguna }: Props) {
const templateRef = useRef<HTMLDivElement>(null)
const [loading, setLoading] = useState(false)
const [idBerkas, setIdBerkas] = useState<number | null>(null)
const rowDate = row.tanggal_upload ? new Date(row.tanggal_upload) : new Date()
const chartData = useMemo(() => build5MonthData(allData, rowDate), [allData, row.tanggal_upload])
const isStunting = row.status_stunting === true
const tanggalCetak = new Date().toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' })
const tanggalUpload = formatTgl(row.tanggal_upload, 'long')
const tanggalLahir = formatTgl(pengguna.tanggal_lahir, 'long')
const handlePrint = async () => {
if (!templateRef.current || loading) return
setLoading(true)
try {
const currentIdBerkas = Date.now()
setIdBerkas(currentIdBerkas)
const { supabase } = await import('@/lib/supabase')
await supabase.from('cetak_balita').insert({
id_berkas: currentIdBerkas,
nama_balita: pengguna.nama_anak,
})
// Tunggu render React selesai (untuk menampilkan ID Berkas)
await new Promise(r => setTimeout(r, 500))
const { default: html2canvas } = await import('html2canvas')
const { default: jsPDF } = await import('jspdf')
const canvas = await html2canvas(templateRef.current, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
logging: false,
})
const imgData = canvas.toDataURL('image/png')
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, 'PNG', 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/png'), 'PNG', 0, 0, pageW, pageH)
yPos += sliceH
}
}
pdf.save(`Laporan_Perkembangan_${pengguna.nama_anak}_${tanggalUpload.replace(/ /g, '_')}.pdf`)
} catch (err: any) {
showSwal.error('Gagal!', `Gagal mencetak PDF: ${err.message}`)
} finally {
setLoading(false)
}
}
return (
<>
{/* ─── Hidden PDF Template ─── */}
<div style={{ position: 'fixed', top: '-9999px', left: '-9999px', zIndex: -1 }}>
<div
ref={templateRef}
style={{
width: 794,
backgroundColor: '#ffffff',
fontFamily: 'Arial, Helvetica, sans-serif',
color: '#111111',
padding: '32px 48px',
boxSizing: 'border-box',
}}
>
{/* ── Header ── */}
<div style={{ borderBottom: '3px solid #111', paddingBottom: 16, marginBottom: 20, 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 Perkembangan Anak
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: 10, color: '#888', marginBottom: 3 }}>Tanggal Cetak</div>
<div style={{ fontSize: 12, fontWeight: 700 }}>{tanggalCetak}</div>
<div style={{ marginTop: 6, fontSize: 10, color: '#888' }}>Sesi Pemeriksaan</div>
<div style={{ fontSize: 12, fontWeight: 700 }}>{tanggalUpload}</div>
</div>
</div>
{/* ── Identitas ── */}
<div style={{ marginBottom: 20 }}>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: 2, textTransform: 'uppercase', color: '#888', marginBottom: 6 }}>
Identitas Balita
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px 32px' }}>
{[
['Nama Ibu / Orang Tua', pengguna.nama_orang_tua],
['Nama Anak', pengguna.nama_anak],
['Alamat', pengguna.alamat ?? '-'],
['Jenis Kelamin', pengguna.jenis_kelamin ?? '-'],
['ID Berkas', idBerkas ? String(idBerkas) : '-'],
['Tanggal Lahir', tanggalLahir],
].map(([label, value], i) => (
label ? (
<div key={i} style={{ borderBottom: '1px solid #eee', paddingBottom: 6 }}>
<div style={{ fontSize: 8, color: '#888', fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1, marginBottom: 2 }}>{label}</div>
<div style={{ fontSize: 12, fontWeight: 600 }}>{value}</div>
</div>
) : <div key={i} />
))}
</div>
</div>
{/* ── Charts ── */}
<div style={{ marginBottom: 20 }}>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: 2, textTransform: 'uppercase', color: '#888', marginBottom: 6 }}>
Grafik Pertumbuhan (5 Bulan Terakhir)
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{/* Panjang */}
<div style={{ border: '1.5 solid #dbeafe', borderRadius: 10, padding: '12px 12px 2px', background: '#eff6ff', borderStyle: 'solid', borderWidth: '1.5px', borderColor: '#dbeafe' }}>
<div style={{ fontSize: 10, fontWeight: 700, color: '#1d4ed8', marginBottom: 6 }}>📏 Panjang Badan (cm)</div>
<ResponsiveContainer width="100%" height={120}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="pdfTinggiGradUser" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#bfdbfe" />
<XAxis dataKey="label" tick={{ fontSize: 8, fontWeight: 600 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 8 }} axisLine={false} tickLine={false} />
<Area type="monotone" dataKey="tinggi" stroke="#3b82f6" strokeWidth={2} fill="url(#pdfTinggiGradUser)"
dot={{ r: 3, fill: '#3b82f6', stroke: 'white', strokeWidth: 2 }}
connectNulls={false}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Berat */}
<div style={{ border: '1.5 solid #d1fae5', borderRadius: 10, padding: '12px 12px 2px', background: '#f0fdf4', borderStyle: 'solid', borderWidth: '1.5px', borderColor: '#d1fae5' }}>
<div style={{ fontSize: 10, fontWeight: 700, color: '#059669', marginBottom: 6 }}> Berat Badan (kg)</div>
<ResponsiveContainer width="100%" height={120}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="pdfBeratGradUser" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#a7f3d0" />
<XAxis dataKey="label" tick={{ fontSize: 8, fontWeight: 600 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 8 }} axisLine={false} tickLine={false} />
<Area type="monotone" dataKey="berat" stroke="#10b981" strokeWidth={2} fill="url(#pdfBeratGradUser)"
dot={{ r: 3, fill: '#10b981', stroke: 'white', strokeWidth: 2 }}
connectNulls={false}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* Z-Score PDF Chart */}
<div style={{ marginTop: 16, border: '1.5px solid #f3e8ff', borderRadius: 10, padding: '12px 12px 2px', background: '#faf5ff', borderStyle: 'solid', borderWidth: '1.5px', borderColor: '#f3e8ff' }}>
<div style={{ fontSize: 10, fontWeight: 700, color: '#9333ea', marginBottom: 6 }}>📈 Z-Score (SD)</div>
<ResponsiveContainer width="100%" height={100}>
<AreaChart data={chartData} margin={{ top: 4, right: 16, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="pdfZscoreGradUser" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#a855f7" stopOpacity={0.3} />
<stop offset="95%" stopColor="#a855f7" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#e9d5ff" />
<XAxis dataKey="label" tick={{ fontSize: 8, fontWeight: 600 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 8 }} axisLine={false} tickLine={false} />
<Area type="monotone" dataKey="zscore" stroke="#9333ea" strokeWidth={2} fill="url(#pdfZscoreGradUser)"
dot={{ r: 3, fill: '#9333ea', stroke: 'white', strokeWidth: 2 }}
connectNulls={false}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* ── Data Pemeriksaan ── */}
<div style={{ marginBottom: 20 }}>
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: 2, textTransform: 'uppercase', color: '#888', marginBottom: 6 }}>
Hasil Pengukuran Sesi Ini
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
<thead>
<tr style={{ background: '#111', color: '#fff' }}>
{['Panjang', 'Berat', 'Z-Score', 'Status Stunting', 'Posyandu', 'Tgl Pemeriksaan'].map(h => (
<th key={h} style={{ padding: '8px 10px', fontWeight: 700, fontSize: 9, letterSpacing: 1, textTransform: 'uppercase', textAlign: 'left' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
<tr style={{ background: '#f9fafb' }}>
<td style={{ padding: '10px', fontWeight: 700 }}>{row.tinggi_badan ?? '-'} cm</td>
<td style={{ padding: '10px', fontWeight: 700 }}>{row.berat_badan ?? '-'} kg</td>
<td style={{ padding: '10px', fontWeight: 700 }}>{row.z_score ?? '-'} SD</td>
<td style={{ padding: '10px' }}>
<span style={{
display: 'inline-block',
padding: '2px 8px',
borderRadius: 20,
fontSize: 10,
fontWeight: 700,
background: isStunting ? '#fef2f2' : '#f0fdf4',
color: isStunting ? '#b91c1c' : '#15803d',
border: `1px solid ${isStunting ? '#fecaca' : '#bbf7d0'}`,
}}>
{isStunting ? '⚠ Stunting' : '✓ Normal'}
</span>
</td>
<td style={{ padding: '10px' }}>{row.nama_posyandu ?? '-'}</td>
<td style={{ padding: '10px' }}>{tanggalUpload}</td>
</tr>
</tbody>
</table>
{/* Pesan AI */}
{row.pesan_ai && (
<div style={{ marginTop: 8, border: '1.5px solid #fde68a', borderRadius: 8, padding: '10px 14px', background: '#fffbeb' }}>
<div style={{ fontSize: 8, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1.5, color: '#92400e', marginBottom: 3 }}>
Rekomendasi / Pesan AI
</div>
<div style={{ fontSize: 11, lineHeight: 1.5, color: '#78350f' }}>{row.pesan_ai}</div>
</div>
)}
</div>
{/* ── Portal Access Info Box ── */}
<div style={{ marginTop: 16, padding: '14px', border: '2px dashed #000', borderRadius: 12, backgroundColor: '#fdfcf0' }}>
<div style={{ fontSize: 10, fontWeight: 900, textTransform: 'uppercase', letterSpacing: 2, color: '#854d0e', display: 'flex', alignItems: 'center', gap: 6 }}>
🌐 Akses Portal Online Orang Tua
</div>
<div style={{ fontSize: 9, color: '#a16207', fontWeight: 500, fontStyle: 'italic', marginBottom: 10 }}>
* Yuk kunjungi website Bapak/Ibu agar mendapatkan informasi perkembangan buah hati Anda.
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 8, color: '#a16207', fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 2 }}>Alamat Website (URL)</div>
<div style={{ fontSize: 11, fontWeight: 800, color: '#000' }}>https://website-cloud-stunting.vercel.app/</div>
</div>
<div style={{ display: 'flex', gap: 16 }}>
<div>
<div style={{ fontSize: 8, color: '#a16207', fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 2 }}>Username</div>
<div style={{ fontSize: 11, fontWeight: 800, color: '#000' }}>{pengguna.username || '-'}</div>
</div>
<div>
<div style={{ fontSize: 8, color: '#a16207', fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 2 }}>Password</div>
<div style={{ fontSize: 11, fontWeight: 800, color: '#000' }}>{pengguna.password || '-'}</div>
</div>
</div>
</div>
</div>
<div style={{ marginTop: 16, paddingTop: 8, borderTop: '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between', fontSize: 8, color: '#bbb' }}>
<span>Dicetak oleh Dashboard Pengguna StuntiScan</span>
<span>Dokumen ini diterbitkan secara otomatis oleh sistem</span>
</div>
</div>
</div>
<button
onClick={handlePrint}
disabled={loading}
className="flex items-center gap-1.5 px-3 py-2 bg-black text-white text-xs font-black rounded-xl hover:bg-gray-800 transition-all shadow-[4px_4px_0px_0px_rgba(0,0,0,0.1)] hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] disabled:opacity-50 disabled:cursor-not-allowed uppercase tracking-widest"
>
<Printer className="w-3.5 h-3.5" />
{loading ? 'Proses...' : 'Unduh PDF'}
</button>
</>
)
}