TKK_E32231405/app/dashboard/kelola-data/[id]/CetakPDFButton.tsx

375 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'
import { supabase } from '@/lib/supabase'
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',
})
}
/** Build 5-month window ending at rowDate (inclusive), no future data */
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 // no future data
)
})
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 CetakPDFButton({ 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)
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 {
// Multi-page
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_${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 Pemeriksaan Balita
</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' }}>Tgl 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
</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 Perkembangan Balita (5 Bulan Terakhir)
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{/* Panjang */}
<div style={{ border: '1.5px solid #dbeafe', borderRadius: 10, padding: '12px 12px 2px', background: '#eff6ff' }}>
<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="pdfTinggiGrad" 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(#pdfTinggiGrad)"
dot={{ r: 3, fill: '#3b82f6', stroke: 'white', strokeWidth: 2 }}
connectNulls={false}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Berat */}
<div style={{ border: '1.5px solid #d1fae5', borderRadius: 10, padding: '12px 12px 2px', background: '#f0fdf4' }}>
<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="pdfBeratGrad" 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(#pdfBeratGrad)"
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="pdfZscoreGradAdmin" 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(#pdfZscoreGradAdmin)"
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 }}>
Data Pemeriksaan
</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>
{/* ── Doc footer ── */}
<div style={{ marginTop: 16, paddingTop: 8, borderTop: '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between', fontSize: 8, color: '#bbb' }}>
<span>Dicetak oleh Sistem Informasi Posyandu</span>
<span>Dokumen ini sah tanpa tanda tangan basah apabila dicetak dari sistem</span>
</div>
</div>
</div>
{/* ─── Visible Button ─── */}
<button
onClick={handlePrint}
disabled={loading}
className="flex items-center gap-1.5 px-2.5 py-1.5 bg-gray-800 text-white text-xs font-bold rounded-lg hover:bg-black transition-all shadow-[2px_2px_0px_0px_rgba(0,0,0,0.3)] hover:shadow-none hover:translate-x-[1px] hover:translate-y-[1px] disabled:opacity-50 disabled:cursor-not-allowed"
>
<Printer className="w-3 h-3" />
{loading ? 'Memproses...' : 'PDF'}
</button>
</>
)
}