diff --git a/app/actions.ts b/app/actions.ts index c8eda0f..7937eea 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -3,6 +3,7 @@ import { supabase } from '@/lib/supabase' import { redirect } from 'next/navigation' import { cookies } from 'next/headers' +import { revalidatePath } from 'next/cache' export async function login(prevState: any, formData: FormData) { const username = formData.get('username') as string @@ -152,3 +153,104 @@ export async function updateAkunBalita(prevState: any, formData: FormData) { return { success: false, message: 'Gagal memperbarui data pengguna. Coba lagi.' } } } + +export async function deleteAkunBalita(id: string) { + if (!id) return { success: false, message: 'ID Akun wajib diisi' } + try { + // Hapus riwayat pengukuran stunting terkait dulu + const { error: errHasil } = await supabase + .from('hasil_stunting_balita') + .delete() + .eq('id_balita', id) + if (errHasil) throw errHasil + + // Hapus akun balita + const { error: errAkun } = await supabase + .from('akun_balita') + .delete() + .eq('id', id) + if (errAkun) throw errAkun + + revalidatePath('/dashboard/manajemen-akun/pengguna') + revalidatePath('/dashboard/kelola-data') + return { success: true, message: 'Akun berhasil dihapus!' } + } catch (error: any) { + console.error('Error deleting akun:', error) + return { success: false, message: error.message || 'Gagal menghapus akun. Coba lagi.' } + } +} + +export async function updateHasilStunting(prevState: any, formData: FormData) { + const id = formData.get('id') as string + const tinggi_badan = formData.get('tinggi_badan') ? Number(formData.get('tinggi_badan')) : null + const berat_badan = formData.get('berat_badan') ? Number(formData.get('berat_badan')) : null + const z_score = formData.get('z_score') ? Number(formData.get('z_score')) : null + const status_stunting = formData.get('status_stunting') === 'true' + const pesan_ai = formData.get('pesan_ai') as string + const tanggal_upload = formData.get('tanggal_upload') as string + const nama_posyandu = formData.get('nama_posyandu') as string + + if (!id) { + return { success: false, message: 'ID Pengukuran tidak valid.' } + } + + try { + const { data, error } = await supabase + .from('hasil_stunting_balita') + .update({ + tinggi_badan, + berat_badan, + z_score, + status_stunting, + pesan_ai, + tanggal_upload: tanggal_upload || null, + nama_posyandu: nama_posyandu || null + }) + .eq('id', id) + .select('id_balita') + .single() + + if (error) throw error + + if (data?.id_balita) { + revalidatePath(`/dashboard/kelola-data/${data.id_balita}`) + } + revalidatePath('/dashboard/kelola-data') + + return { success: true, message: 'Data pengukuran berhasil diperbarui!' } + } catch (error: any) { + console.error('Error updating stunting record:', error) + return { success: false, message: error.message || 'Gagal memperbarui data pengukuran.' } + } +} + +export async function deleteHasilStunting(id: number) { + if (!id) return { success: false, message: 'ID Pengukuran tidak valid.' } + + try { + // Ambil id_balita untuk revalidasi path + const { data: record } = await supabase + .from('hasil_stunting_balita') + .select('id_balita') + .eq('id', id) + .single() + + const { error } = await supabase + .from('hasil_stunting_balita') + .delete() + .eq('id', id) + + if (error) throw error + + if (record?.id_balita) { + revalidatePath(`/dashboard/kelola-data/${record.id_balita}`) + } + revalidatePath('/dashboard/kelola-data') + + return { success: true, message: 'Data pengukuran berhasil dihapus!' } + } catch (error: any) { + console.error('Error deleting stunting record:', error) + return { success: false, message: error.message || 'Gagal menghapus data pengukuran.' } + } +} + diff --git a/app/dashboard/kelola-data/[id]/HasilStuntingTable.tsx b/app/dashboard/kelola-data/[id]/HasilStuntingTable.tsx index 6b79721..d298f73 100644 --- a/app/dashboard/kelola-data/[id]/HasilStuntingTable.tsx +++ b/app/dashboard/kelola-data/[id]/HasilStuntingTable.tsx @@ -1,8 +1,10 @@ 'use client' import { useState, useMemo } from 'react' -import { Activity, ChevronDown } from 'lucide-react' +import { Activity, ChevronDown, Edit, Trash2, X, Loader2 } from 'lucide-react' import { CetakPDFButton } from './CetakPDFButton' +import { deleteHasilStunting, updateHasilStunting } from '@/app/actions' +import { showSwal } from '@/lib/swal' interface HasilStunting { id: number @@ -26,11 +28,34 @@ interface Pengguna { interface Props { data: HasilStunting[] pengguna: Pengguna + posyanduList: { nama_posyandu: string }[] } const START_YEAR = 2026 -export function HasilStuntingTable({ data, pengguna }: Props) { +export function HasilStuntingTable({ data, pengguna, posyanduList }: Props) { + const [editRecord, setEditRecord] = useState(null) + + const handleDelete = async (id: number) => { + const confirmed = await showSwal.confirm( + 'Hapus Data Pengukuran?', + 'Data pengukuran ini akan dihapus permanen dari riwayat balita!' + ) + + if (confirmed.isConfirmed) { + try { + const res = await deleteHasilStunting(id) + if (res.success) { + await showSwal.success('Berhasil!', res.message) + } else { + showSwal.error('Gagal!', res.message) + } + } catch (err: any) { + showSwal.error('Gagal!', err.message || 'Terjadi kesalahan.') + } + } + } + const availableYears = useMemo(() => { const currentYear = new Date().getFullYear() const dataYears = Array.from( @@ -89,101 +114,307 @@ export function HasilStuntingTable({ data, pengguna }: Props) { {/* Table */}
+
+
+ {/* Header */} +
+ # + Panjang + Berat + Z-Score + Status + Pesan AI + Posyandu + Tgl Upload + Aksi +
+ + {filtered.length === 0 ? ( +
+ +

Tidak ada data untuk tahun {selectedYear}

+
+ ) : ( + filtered.map((row, idx) => { + const isStunting = row.status_stunting === true + + return ( +
+ {/* Index */} +
+ {idx + 1} +
+ + {/* Panjang Badan */} +
+ {row.tinggi_badan ?? '-'} + {row.tinggi_badan && cm} +
+ + {/* Berat Badan */} +
+ {row.berat_badan ?? '-'} + {row.berat_badan && kg} +
+ + {/* Z-Score */} +
+ {row.z_score ?? '-'} + {row.z_score !== null && SD} +
+ + {/* Status Stunting */} +
+ {row.status_stunting === null ? ( + + ) : ( + + {isStunting ? '⚠ Stunting' : '✓ Normal'} + + )} +
+ + {/* Pesan AI — truncated */} +
+ {row.pesan_ai ? ( +

+ {row.pesan_ai.length > 80 + ? row.pesan_ai.slice(0, 80) + '..........' + : row.pesan_ai} +

+ ) : ( + Tidak ada pesan + )} +
+ + {/* Nama Posyandu */} +
+ {row.nama_posyandu ?? '-'} +
+ + {/* Tanggal Upload */} +
+ {formatDate(row.tanggal_upload)} +
+ + {/* Aksi: Cetak, Edit, Hapus */} +
+ + + +
+
+ ) + }) + )} +
+
+
+ + {/* Edit Modal */} + {editRecord && ( + setEditRecord(null)} + record={editRecord} + posyanduList={posyanduList} + /> + )} + + ) +} + +interface EditModalProps { + isOpen: boolean + onClose: () => void + record: HasilStunting + posyanduList: { nama_posyandu: string }[] +} + +function EditHasilStuntingModal({ isOpen, onClose, record, posyanduList }: EditModalProps) { + const [tinggi, setTinggi] = useState(record.tinggi_badan?.toString() || '') + const [berat, setBerat] = useState(record.berat_badan?.toString() || '') + const [zScore, setZScore] = useState(record.z_score?.toString() || '') + const [statusStunting, setStatusStunting] = useState(record.status_stunting === true ? 'true' : 'false') + const [pesanAi, setPesanAi] = useState(record.pesan_ai || '') + const [tanggalUpload, setTanggalUpload] = useState(record.tanggal_upload ? record.tanggal_upload.split('T')[0] : '') + const [namaPosyandu, setNamaPosyandu] = useState(record.nama_posyandu || '') + const [loading, setLoading] = useState(false) + + if (!isOpen) return null + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + + const formData = new FormData() + formData.append('id', record.id.toString()) + formData.append('tinggi_badan', tinggi) + formData.append('berat_badan', berat) + formData.append('z_score', zScore) + formData.append('status_stunting', statusStunting) + formData.append('pesan_ai', pesanAi) + formData.append('tanggal_upload', tanggalUpload) + formData.append('nama_posyandu', namaPosyandu) + + try { + const res = await updateHasilStunting(null, formData) + if (res.success) { + await showSwal.success('Berhasil!', res.message) + onClose() + } else { + showSwal.error('Gagal!', res.message) + } + } catch (err: any) { + showSwal.error('Gagal!', err.message || 'Terjadi kesalahan.') + } finally { + setLoading(false) + } + } + + return ( +
+
{/* Header */} -
- # - Panjang - Berat - Z-Score - Status - Pesan AI - Posyandu - Tgl Upload - Aksi +
+
+ +

Edit Data Pengukuran

+
+
- {filtered.length === 0 ? ( -
- -

Tidak ada data untuk tahun {selectedYear}

+ {/* Form */} +
+
+
+ + setTinggi(e.target.value)} + className="w-full border-2 border-gray-200 focus:border-black rounded-lg p-2.5 text-sm font-semibold focus:outline-none transition-colors" + required + /> +
+
+ + setBerat(e.target.value)} + className="w-full border-2 border-gray-200 focus:border-black rounded-lg p-2.5 text-sm font-semibold focus:outline-none transition-colors" + required + /> +
- ) : ( - filtered.map((row, idx) => { - const isStunting = row.status_stunting === true - return ( -
+
+ + setZScore(e.target.value)} + className="w-full border-2 border-gray-200 focus:border-black rounded-lg p-2.5 text-sm font-semibold focus:outline-none transition-colors" + required + /> +
+
+ + +
+
- {/* Panjang Badan */} -
- {row.tinggi_badan ?? '-'} - {row.tinggi_badan && cm} -
+
+
+ + +
+
+ + setTanggalUpload(e.target.value)} + className="w-full border-2 border-gray-200 focus:border-black rounded-lg p-2.5 text-sm font-semibold focus:outline-none transition-colors" + required + /> +
+
- {/* Berat Badan */} -
- {row.berat_badan ?? '-'} - {row.berat_badan && kg} -
+
+ +