add fix bug
This commit is contained in:
parent
96ecb499b9
commit
8fc5257458
|
|
@ -16,6 +16,7 @@ interface HasilItem {
|
|||
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
|
||||
|
|
@ -72,7 +73,12 @@ function build5MonthData(allData: HasilItem[], rowDate: Date) {
|
|||
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 }
|
||||
return {
|
||||
label,
|
||||
tinggi: match?.tinggi_badan ?? null,
|
||||
berat: match?.berat_badan ?? null,
|
||||
zscore: match?.z_score ?? null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +191,7 @@ export function CetakInstanModal() {
|
|||
// --- 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, 600)) // 600ms buffer for Recharts animations/stable DOM
|
||||
await new Promise(r => setTimeout(r, 1000)) // 1s buffer for stable DOM & Recharts
|
||||
|
||||
if (!templateRef.current) continue
|
||||
|
||||
|
|
@ -197,14 +203,21 @@ export function CetakInstanModal() {
|
|||
logging: false,
|
||||
})
|
||||
|
||||
const imgData = canvas.toDataURL('image/png')
|
||||
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, 'PNG', 0, 0, pageW, imgH)
|
||||
pdf.addImage(imgData, 'JPEG', 0, 0, pageW, imgH)
|
||||
} else {
|
||||
let yPos = 0
|
||||
const sliceH = canvas.width * (pageH / pageW)
|
||||
|
|
@ -215,7 +228,7 @@ export function CetakInstanModal() {
|
|||
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)
|
||||
pdf.addImage(sliceCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', 0, 0, pageW, pageH)
|
||||
yPos += sliceH
|
||||
}
|
||||
}
|
||||
|
|
@ -242,7 +255,7 @@ export function CetakInstanModal() {
|
|||
|
||||
clearInterval(timer)
|
||||
setStep('done')
|
||||
await showSwal.success('Selesai!', `Berhasil mencetak ${progress.total} file PDF.`)
|
||||
await showSwal.success('Selesai!', `Berhasil mencetak ${targets.length} file PDF.`)
|
||||
handleClose()
|
||||
} catch (err: any) {
|
||||
clearInterval(timer)
|
||||
|
|
@ -410,7 +423,7 @@ export function CetakInstanModal() {
|
|||
|
||||
{/* ─── HIDDEN PDF TEMPLATE (Rich HTML) ─── */}
|
||||
{activePrintData && (
|
||||
<div style={{ position: 'fixed', top: '-9999px', left: '-9999px', zIndex: -1 }}>
|
||||
<div style={{ position: 'fixed', left: 0, top: 0, opacity: 0, pointerEvents: 'none', zIndex: -100, width: 'fit-content' }}>
|
||||
<div
|
||||
ref={templateRef}
|
||||
style={{
|
||||
|
|
@ -463,16 +476,10 @@ export function CetakInstanModal() {
|
|||
<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 }}>
|
||||
<defs>
|
||||
<linearGradient id="instanTinggi" 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" fontSize={9} axisLine={false} tickLine={false} />
|
||||
<YAxis fontSize={9} axisLine={false} tickLine={false} />
|
||||
<Area type="monotone" dataKey="tinggi" stroke="#3b82f6" strokeWidth={2} fill="url(#instanTinggi)" dot={{ r: 4, fill: '#3b82f6', stroke: 'white' }} />
|
||||
<Area type="monotone" dataKey="tinggi" stroke="#3b82f6" strokeWidth={2} fill="none" dot={{ r: 4, fill: '#3b82f6', stroke: 'white' }} isAnimationActive={false} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
|
@ -480,20 +487,26 @@ export function CetakInstanModal() {
|
|||
<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 }}>
|
||||
<defs>
|
||||
<linearGradient id="instanBerat" 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" fontSize={9} axisLine={false} tickLine={false} />
|
||||
<YAxis fontSize={9} axisLine={false} tickLine={false} />
|
||||
<Area type="monotone" dataKey="berat" stroke="#10b981" strokeWidth={2} fill="url(#instanBerat)" dot={{ r: 4, fill: '#10b981', stroke: 'white' }} />
|
||||
<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 */}
|
||||
|
|
@ -502,7 +515,7 @@ export function CetakInstanModal() {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#111', color: '#fff' }}>
|
||||
{['Tinggi', 'Berat', 'Status', 'Posyandu', 'Tgl Upload'].map(h => (
|
||||
{['Tinggi', 'Berat', 'Z-Score', 'Status', 'Posyandu', 'Tgl Upload'].map(h => (
|
||||
<th key={h} style={{ padding: '8px 12px', textAlign: 'left', fontSize: 10 }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -511,6 +524,7 @@ export function CetakInstanModal() {
|
|||
<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'}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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
|
||||
|
|
@ -68,6 +69,7 @@ function build5MonthData(allData: HasilItem[], rowDate: Date) {
|
|||
label,
|
||||
tinggi: match?.tinggi_badan ?? null,
|
||||
berat: match?.berat_badan ?? null,
|
||||
zscore: match?.z_score ?? null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -210,6 +212,7 @@ export function CetakPDFButton({ row, allData, pengguna }: Props) {
|
|||
<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>
|
||||
|
|
@ -231,11 +234,34 @@ export function CetakPDFButton({ row, allData, pengguna }: Props) {
|
|||
<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 ── */}
|
||||
|
|
@ -246,15 +272,16 @@ export function CetakPDFButton({ row, allData, pengguna }: Props) {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#111', color: '#fff' }}>
|
||||
{['Tinggi Badan', 'Berat Badan', 'Status Stunting', 'Posyandu', 'Tgl Pemeriksaan'].map(h => (
|
||||
{['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 ?? '-'} {row.tinggi_badan ? 'cm' : ''}</td>
|
||||
<td style={{ padding: '10px', fontWeight: 700 }}>{row.berat_badan ?? '-'} {row.berat_badan ? 'kg' : ''}</td>
|
||||
<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',
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface HasilStunting {
|
|||
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
|
||||
|
|
@ -89,10 +90,11 @@ export function HasilStuntingTable({ data, pengguna }: Props) {
|
|||
{/* Table */}
|
||||
<div className="rounded-xl border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="grid grid-cols-[36px_88px_88px_100px_1fr_108px_120px_90px] bg-black text-white px-4 py-3 text-[10px] font-bold uppercase tracking-widest">
|
||||
<div className="grid grid-cols-[36px_88px_88px_88px_100px_1fr_108px_110px_90px] bg-black text-white px-4 py-3 text-[10px] font-bold uppercase tracking-widest">
|
||||
<span className="text-center text-gray-500">#</span>
|
||||
<span className="text-center">Tinggi</span>
|
||||
<span className="text-center">Berat</span>
|
||||
<span className="text-center">Z-Score</span>
|
||||
<span className="text-center">Status</span>
|
||||
<span>Pesan AI</span>
|
||||
<span className="text-center">Posyandu</span>
|
||||
|
|
@ -112,7 +114,7 @@ export function HasilStuntingTable({ data, pengguna }: Props) {
|
|||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
className={`grid grid-cols-[36px_88px_88px_100px_1fr_108px_120px_90px] items-center px-4 py-3 border-b border-gray-100 text-sm transition-colors ${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50/40'} hover:bg-orange-50/30`}
|
||||
className={`grid grid-cols-[36px_88px_88px_88px_100px_1fr_108px_110px_90px] items-center px-4 py-3 border-b border-gray-100 text-sm transition-colors ${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50/40'} hover:bg-orange-50/30`}
|
||||
>
|
||||
{/* Index */}
|
||||
<div className="flex justify-center">
|
||||
|
|
@ -131,6 +133,12 @@ export function HasilStuntingTable({ data, pengguna }: Props) {
|
|||
{row.berat_badan && <span className="text-xs text-gray-400 ml-0.5">kg</span>}
|
||||
</div>
|
||||
|
||||
{/* Z-Score */}
|
||||
<div className="text-center">
|
||||
<span className="font-bold">{row.z_score ?? '-'}</span>
|
||||
{row.z_score !== null && <span className="text-xs text-gray-400 ml-0.5">SD</span>}
|
||||
</div>
|
||||
|
||||
{/* Status Stunting */}
|
||||
<div className="flex justify-center">
|
||||
{row.status_stunting === null ? (
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ import {
|
|||
XAxis, YAxis, CartesianGrid, Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import { Ruler, Weight, ChevronDown } from 'lucide-react'
|
||||
import { Ruler, Weight, ChevronDown, Activity } from 'lucide-react'
|
||||
|
||||
interface HasilItem {
|
||||
tinggi_badan: number | null
|
||||
berat_badan: number | null
|
||||
z_score: number | null
|
||||
tanggal_upload: string | null
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ export function PerkembanganChart({ data }: Props) {
|
|||
_date: new Date(d.tanggal_upload!).getTime(),
|
||||
tinggi: d.tinggi_badan,
|
||||
berat: d.berat_badan,
|
||||
zscore: d.z_score,
|
||||
}))
|
||||
.sort((a, b) => a._date - b._date)
|
||||
}, [data, selectedYear])
|
||||
|
|
@ -189,6 +191,48 @@ export function PerkembanganChart({ data }: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Z-Score Chart */}
|
||||
<div className="rounded-xl border-2 border-purple-100 bg-purple-50/30 p-4 md:col-span-2">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-purple-100 border border-purple-200 flex items-center justify-center">
|
||||
<Activity className="w-4 h-4 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-purple-800">Z-Score (SD)</p>
|
||||
<p className="text-[10px] text-purple-400">Standar Deviasi Pertumbuhan</p>
|
||||
</div>
|
||||
</div>
|
||||
{!hasData ? (
|
||||
<div className="h-36 flex items-center justify-center text-gray-300 text-xs text-center border-2 border-dashed border-purple-100/50 rounded-lg">
|
||||
Tidak ada data untuk tahun {selectedYear}
|
||||
</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="zscoreGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#9333ea" stopOpacity={0.25} />
|
||||
<stop offset="95%" stopColor="#9333ea" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f3e8ff" />
|
||||
<XAxis dataKey="label" tick={{ fontSize: 10, fontWeight: 600 }} axisLine={false} tickLine={false} />
|
||||
<YAxis tick={{ fontSize: 10 }} axisLine={false} tickLine={false} domain={['auto', 'auto']} />
|
||||
<Tooltip content={<MiniTooltip unit="SD" color="#9333ea" />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="zscore"
|
||||
stroke="#9333ea"
|
||||
strokeWidth={2.5}
|
||||
fill="url(#zscoreGrad)"
|
||||
dot={{ r: 4, fill: '#9333ea', stroke: 'white', strokeWidth: 2 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default async function DetailPenggunaKelolaPage({ params }: Props) {
|
|||
// Fetch hasil pengukuran stunting milik balita ini
|
||||
const { data: hasilData } = await supabase
|
||||
.from('hasil_stunting_balita')
|
||||
.select('id, tinggi_badan, berat_badan, status_stunting, pesan_ai, tanggal_upload, nama_posyandu')
|
||||
.select('id, tinggi_badan, berat_badan, z_score, status_stunting, pesan_ai, tanggal_upload, nama_posyandu')
|
||||
.eq('id_balita', pengguna.id)
|
||||
.order('tanggal_upload', { ascending: false })
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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
|
||||
|
|
@ -67,6 +68,7 @@ function build5MonthData(allData: HasilItem[], rowDate: Date) {
|
|||
label,
|
||||
tinggi: match?.tinggi_badan ?? null,
|
||||
berat: match?.berat_badan ?? null,
|
||||
zscore: match?.z_score ?? null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -208,6 +210,7 @@ export function ExportPDFButton({ row, allData, pengguna }: Props) {
|
|||
<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>
|
||||
|
|
@ -229,11 +232,34 @@ export function ExportPDFButton({ row, allData, pengguna }: Props) {
|
|||
<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 ── */}
|
||||
|
|
@ -244,15 +270,16 @@ export function ExportPDFButton({ row, allData, pengguna }: Props) {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#111', color: '#fff' }}>
|
||||
{['Tinggi Badan', 'Berat Badan', 'Status Stunting', 'Posyandu', 'Tgl Pemeriksaan'].map(h => (
|
||||
{['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 ?? '-'} {row.tinggi_badan ? 'cm' : ''}</td>
|
||||
<td style={{ padding: '10px', fontWeight: 700 }}>{row.berat_badan ?? '-'} {row.berat_badan ? 'kg' : ''}</td>
|
||||
<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',
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ import {
|
|||
XAxis, YAxis, CartesianGrid, Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import { Ruler, Weight, ChevronDown } from 'lucide-react'
|
||||
import { Ruler, Weight, ChevronDown, Activity } from 'lucide-react'
|
||||
|
||||
interface HasilItem {
|
||||
tinggi_badan: number | null
|
||||
berat_badan: number | null
|
||||
z_score: number | null
|
||||
tanggal_upload: string | null
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ export function GrowthChart({ data }: Props) {
|
|||
_date: new Date(d.tanggal_upload!).getTime(),
|
||||
tinggi: d.tinggi_badan,
|
||||
berat: d.berat_badan,
|
||||
zscore: d.z_score,
|
||||
}))
|
||||
.sort((a, b) => a._date - b._date)
|
||||
}, [data, selectedYear])
|
||||
|
|
@ -89,7 +91,7 @@ export function GrowthChart({ data }: Props) {
|
|||
Grafik Pertumbuhan Anak
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest">
|
||||
Statistik Tinggi & Berat {selectedYear}
|
||||
Statistik Tinggi, Berat & Z-Score {selectedYear}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -156,6 +158,7 @@ export function GrowthChart({ data }: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Weight Chart */}
|
||||
<div className="rounded-2xl border-2 border-emerald-100 bg-emerald-50/20 p-6 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -200,6 +203,52 @@ export function GrowthChart({ data }: Props) {
|
|||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Z-Score Chart */}
|
||||
<div className="rounded-2xl border-2 border-purple-100 bg-purple-50/20 p-6 flex flex-col gap-4 md:col-span-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-100 border border-purple-200 flex items-center justify-center shadow-sm">
|
||||
<Activity className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-black text-purple-800 uppercase tracking-widest">Z-Score (SD)</p>
|
||||
<p className="text-[10px] text-purple-400 font-bold uppercase tracking-tighter">Standar Deviasi Pertumbuhan</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hasData ? (
|
||||
<div className="h-44 flex flex-col items-center justify-center text-gray-300 gap-2 border-2 border-dashed border-purple-100 rounded-xl">
|
||||
<Activity className="w-8 h-8 opacity-20" />
|
||||
<p className="text-[10px] font-black uppercase">Data belum tersedia</p>
|
||||
</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={180}>
|
||||
<AreaChart data={chartData} margin={{ top: 8, right: 8, left: -25, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="zscoreGradUser" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#9333ea" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#9333ea" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f3e8ff" />
|
||||
<XAxis dataKey="label" tick={{ fontSize: 10, fontWeight: 800, fill: '#9333ea' }} axisLine={false} tickLine={false} />
|
||||
<YAxis tick={{ fontSize: 10, fontWeight: 700, fill: '#94a3b8' }} axisLine={false} tickLine={false} domain={['auto', 'auto']} />
|
||||
<Tooltip content={<MiniTooltip unit="SD" color="#9333ea" />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="zscore"
|
||||
stroke="#9333ea"
|
||||
strokeWidth={4}
|
||||
fill="url(#zscoreGradUser)"
|
||||
dot={{ r: 5, fill: '#9333ea', stroke: 'white', strokeWidth: 2 }}
|
||||
activeDot={{ r: 8, strokeWidth: 4 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface HasilStunting {
|
|||
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
|
||||
|
|
@ -88,9 +89,10 @@ export function StuntingTable({ data, pengguna }: Props) {
|
|||
<div className="overflow-x-auto">
|
||||
<div className="min-w-[1000px]">
|
||||
{/* Header */}
|
||||
<div className="grid grid-cols-[100px_100px_140px_1fr_160px_160px_120px] bg-black text-white px-6 py-4 text-[10px] font-black uppercase tracking-widest">
|
||||
<div className="grid grid-cols-[100px_100px_100px_140px_1fr_160px_160px_120px] bg-black text-white px-6 py-4 text-[10px] font-black uppercase tracking-widest">
|
||||
<span className="text-center">Tinggi</span>
|
||||
<span className="text-center">Berat</span>
|
||||
<span className="text-center">Z-Score</span>
|
||||
<span className="text-center">Status</span>
|
||||
<span>Pesan / Rekomendasi</span>
|
||||
<span className="text-center">Posyandu</span>
|
||||
|
|
@ -111,7 +113,7 @@ export function StuntingTable({ data, pengguna }: Props) {
|
|||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
className="grid grid-cols-[100px_100px_140px_1fr_160px_160px_120px] items-center px-6 py-6 transition-colors hover:bg-gray-50/50"
|
||||
className="grid grid-cols-[100px_100px_100px_140px_1fr_160px_160px_120px] items-center px-6 py-6 transition-colors hover:bg-gray-50/50"
|
||||
>
|
||||
{/* Tinggi Badan */}
|
||||
<div className="text-center">
|
||||
|
|
@ -125,6 +127,12 @@ export function StuntingTable({ data, pengguna }: Props) {
|
|||
{row.berat_badan && <span className="text-[10px] text-gray-400 ml-1 font-bold">kg</span>}
|
||||
</div>
|
||||
|
||||
{/* Z-Score */}
|
||||
<div className="text-center">
|
||||
<span className="font-black text-lg text-black">{row.z_score ?? '-'}</span>
|
||||
{row.z_score !== null && <span className="text-[10px] text-gray-400 ml-1 font-bold">SD</span>}
|
||||
</div>
|
||||
|
||||
{/* Status Stunting */}
|
||||
<div className="flex justify-center">
|
||||
{row.status_stunting === null ? (
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export default async function UserPerkembanganPage() {
|
|||
// Fetch measurement history
|
||||
const { data: hasilData } = await supabase
|
||||
.from('hasil_stunting_balita')
|
||||
.select('id, tinggi_badan, berat_badan, status_stunting, pesan_ai, tanggal_upload, nama_posyandu')
|
||||
.select('id, tinggi_badan, berat_badan, z_score, status_stunting, pesan_ai, tanggal_upload, nama_posyandu')
|
||||
.eq('id_balita', pengguna.id)
|
||||
.order('tanggal_upload', { ascending: false })
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue