369 lines
21 KiB
TypeScript
369 lines
21 KiB
TypeScript
'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 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 { 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 ?? '-'],
|
||
['', ''],
|
||
['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 }}>
|
||
{/* Tinggi */}
|
||
<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 }}>📏 Tinggi 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' }}>
|
||
{['Tinggi', '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>
|
||
|
||
{/* ── WhatsApp Info Box ── */}
|
||
<div style={{ marginTop: 16, padding: '10px 14px', borderRadius: 10, backgroundColor: '#f0fdf4', border: '1px solid #bbf7d0', display: 'flex', alignItems: 'flex-start', gap: 10 }}>
|
||
<div style={{ fontSize: 16 }}>📱</div>
|
||
<div>
|
||
<div style={{ fontSize: 9, fontWeight: 800, color: '#166534', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 2 }}>Layanan Informasi WhatsApp</div>
|
||
<div style={{ fontSize: 10, lineHeight: 1.5, color: '#14532d' }}>
|
||
Untuk orang tua yang tidak memiliki akun WhatsApp, yuk segera buat akun karena kami melayani layanan penyampaian informasi hasil stunting dengan menggunakan WhatsApp agar mendapatkan informasi lebih cepat.
|
||
</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>
|
||
</>
|
||
)
|
||
}
|