186 lines
9.9 KiB
TypeScript
186 lines
9.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useMemo } from 'react'
|
|
import { Activity, ChevronDown } from 'lucide-react'
|
|
import { ExportPDFButton } from './ExportPDFButton'
|
|
|
|
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
|
|
nama_posyandu: string | null
|
|
}
|
|
|
|
interface Pengguna {
|
|
nama_orang_tua: string
|
|
alamat: string | null
|
|
nama_anak: string
|
|
jenis_kelamin: string | null
|
|
tanggal_lahir: string | null
|
|
}
|
|
|
|
interface Props {
|
|
data: HasilStunting[]
|
|
pengguna: Pengguna
|
|
}
|
|
|
|
const START_YEAR = 2026
|
|
|
|
export function StuntingTable({ data, pengguna }: Props) {
|
|
const availableYears = useMemo(() => {
|
|
const currentYear = new Date().getFullYear()
|
|
const dataYears = Array.from(
|
|
new Set(data.map(d => d.tanggal_upload ? new Date(d.tanggal_upload).getFullYear() : null).filter(Boolean))
|
|
) as number[]
|
|
const maxYear = Math.max(currentYear, ...dataYears, START_YEAR)
|
|
return Array.from(
|
|
new Set([
|
|
...Array.from({ length: maxYear - START_YEAR + 1 }, (_, i) => START_YEAR + i),
|
|
...dataYears,
|
|
])
|
|
).filter(y => y >= START_YEAR).sort((a, b) => a - b)
|
|
}, [data])
|
|
|
|
const [selectedYear, setSelectedYear] = useState<number>(availableYears[availableYears.length - 1] ?? START_YEAR)
|
|
|
|
const filtered = useMemo(() => {
|
|
return data.filter(d => {
|
|
if (!d.tanggal_upload) return false
|
|
return new Date(d.tanggal_upload).getFullYear() === selectedYear
|
|
})
|
|
}, [data, selectedYear])
|
|
|
|
const formatDate = (d: string | null) => {
|
|
if (!d) return '-'
|
|
return new Date(d).toLocaleDateString('id-ID', {
|
|
day: 'numeric', month: 'short', year: 'numeric'
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<Activity className="w-4 h-4 text-gray-500" />
|
|
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Riwayat Pengukuran</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs font-bold uppercase tracking-widest text-gray-400">Periode:</span>
|
|
<div className="relative">
|
|
<select
|
|
value={selectedYear}
|
|
onChange={e => setSelectedYear(Number(e.target.value))}
|
|
className="appearance-none border-2 border-black rounded-lg pl-3 pr-8 py-1.5 text-sm font-bold bg-white focus:outline-none shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] cursor-pointer"
|
|
>
|
|
{availableYears.map(year => (
|
|
<option key={year} value={year}>{year}</option>
|
|
))}
|
|
</select>
|
|
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 pointer-events-none text-gray-500" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-xl border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<div className="min-w-[1000px]">
|
|
{/* Header */}
|
|
<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">Panjang</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>
|
|
<span className="text-center">Tanggal</span>
|
|
<span className="text-center">Laporan</span>
|
|
</div>
|
|
|
|
{filtered.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-20 gap-3 text-gray-300 bg-white">
|
|
<Activity className="w-12 h-12 opacity-20" />
|
|
<p className="text-sm text-gray-400 font-black uppercase tracking-widest">Data belum tersedia tahun {selectedYear}</p>
|
|
</div>
|
|
) : (
|
|
<div className="divide-y-2 divide-gray-100 bg-white">
|
|
{filtered.map((row) => {
|
|
const isStunting = row.status_stunting === true
|
|
|
|
return (
|
|
<div
|
|
key={row.id}
|
|
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"
|
|
>
|
|
{/* Panjang Badan */}
|
|
<div className="text-center">
|
|
<span className="font-black text-lg text-black">{row.tinggi_badan ?? '-'}</span>
|
|
{row.tinggi_badan && <span className="text-[10px] text-gray-400 ml-1 font-bold">cm</span>}
|
|
</div>
|
|
|
|
{/* Berat Badan */}
|
|
<div className="text-center">
|
|
<span className="font-black text-lg text-black">{row.berat_badan ?? '-'}</span>
|
|
{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 ? (
|
|
<span className="text-xs text-gray-300 font-bold">—</span>
|
|
) : (
|
|
<span className={`inline-flex items-center px-4 py-1.5 rounded-full text-[10px] font-black border uppercase tracking-widest ${isStunting
|
|
? 'bg-red-50 border-red-200 text-red-700 shadow-[2px_2px_0px_0px_rgba(239,68,68,0.1)]'
|
|
: 'bg-emerald-50 border-emerald-200 text-emerald-700 shadow-[2px_2px_0px_0px_rgba(16,185,129,0.1)]'
|
|
}`}>
|
|
{isStunting ? '⚠ Stunting' : '✓ Normal'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Pesan AI — truncated */}
|
|
<div className="pr-6">
|
|
{row.pesan_ai ? (
|
|
<p className="text-[11px] text-gray-600 leading-relaxed font-medium line-clamp-2">
|
|
{row.pesan_ai}
|
|
</p>
|
|
) : (
|
|
<span className="text-xs text-gray-300 italic font-medium">Data sedang diproses...</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Nama Posyandu */}
|
|
<div className="text-center">
|
|
<span className="text-[11px] text-gray-900 font-black uppercase tracking-tight">{row.nama_posyandu ?? '-'}</span>
|
|
</div>
|
|
|
|
{/* Tanggal Upload */}
|
|
<div className="text-center">
|
|
<span className="text-[11px] text-gray-500 font-bold">{formatDate(row.tanggal_upload)}</span>
|
|
</div>
|
|
|
|
{/* Aksi: Cetak PDF */}
|
|
<div className="flex justify-center">
|
|
<ExportPDFButton row={row} allData={data} pengguna={pengguna} />
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|