add fix bug

This commit is contained in:
oxel 2026-04-18 11:12:38 +07:00
parent 96ecb499b9
commit 8fc5257458
9 changed files with 214 additions and 37 deletions

View File

@ -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'}

View File

@ -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',

View File

@ -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 ? (

View File

@ -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>
)

View File

@ -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 })

View File

@ -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',

View File

@ -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>
)

View File

@ -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 ? (

View File

@ -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 })