FIRST COMMIT EVER
This commit is contained in:
parent
6dec5a0470
commit
653ea8afe6
102
app/actions.ts
102
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.' }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HasilStunting | null>(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 */}
|
||||
<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-[1050px]">
|
||||
{/* Header */}
|
||||
<div className="grid grid-cols-[36px_88px_88px_88px_100px_1fr_108px_110px_140px] 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">Panjang</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>
|
||||
<span className="text-center">Tgl Upload</span>
|
||||
<span className="text-center">Aksi</span>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2 text-gray-300">
|
||||
<Activity className="w-10 h-10 opacity-30" />
|
||||
<p className="text-sm text-gray-400 font-semibold">Tidak ada data untuk tahun {selectedYear}</p>
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((row, idx) => {
|
||||
const isStunting = row.status_stunting === true
|
||||
|
||||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
className={`grid grid-cols-[36px_88px_88px_88px_100px_1fr_108px_110px_140px] 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">
|
||||
<span className="text-xs text-gray-400 font-bold">{idx + 1}</span>
|
||||
</div>
|
||||
|
||||
{/* Panjang Badan */}
|
||||
<div className="text-center">
|
||||
<span className="font-bold">{row.tinggi_badan ?? '-'}</span>
|
||||
{row.tinggi_badan && <span className="text-xs text-gray-400 ml-0.5">cm</span>}
|
||||
</div>
|
||||
|
||||
{/* Berat Badan */}
|
||||
<div className="text-center">
|
||||
<span className="font-bold">{row.berat_badan ?? '-'}</span>
|
||||
{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 ? (
|
||||
<span className="text-xs text-gray-300">—</span>
|
||||
) : (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold border ${isStunting
|
||||
? 'bg-red-50 border-red-200 text-red-700'
|
||||
: 'bg-emerald-50 border-emerald-200 text-emerald-700'
|
||||
}`}>
|
||||
{isStunting ? '⚠ Stunting' : '✓ Normal'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pesan AI — truncated */}
|
||||
<div className="pr-2">
|
||||
{row.pesan_ai ? (
|
||||
<p className="text-xs text-gray-600 leading-relaxed">
|
||||
{row.pesan_ai.length > 80
|
||||
? row.pesan_ai.slice(0, 80) + '..........'
|
||||
: row.pesan_ai}
|
||||
</p>
|
||||
) : (
|
||||
<span className="text-xs text-gray-300 italic">Tidak ada pesan</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nama Posyandu */}
|
||||
<div className="text-center">
|
||||
<span className="text-xs text-gray-600">{row.nama_posyandu ?? '-'}</span>
|
||||
</div>
|
||||
|
||||
{/* Tanggal Upload */}
|
||||
<div className="text-center">
|
||||
<span className="text-xs text-gray-500">{formatDate(row.tanggal_upload)}</span>
|
||||
</div>
|
||||
|
||||
{/* Aksi: Cetak, Edit, Hapus */}
|
||||
<div className="flex justify-center items-center gap-1.5">
|
||||
<CetakPDFButton row={row} allData={data} pengguna={pengguna} />
|
||||
<button
|
||||
onClick={() => setEditRecord(row)}
|
||||
className="p-1.5 bg-amber-500 hover:bg-amber-600 text-white rounded-lg transition-all shadow-[1.5px_1.5px_0px_0px_rgba(0,0,0,0.3)] hover:shadow-none hover:translate-x-[0.5px] hover:translate-y-[0.5px]"
|
||||
title="Edit Data"
|
||||
>
|
||||
<Edit className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(row.id)}
|
||||
className="p-1.5 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-all shadow-[1.5px_1.5px_0px_0px_rgba(0,0,0,0.3)] hover:shadow-none hover:translate-x-[0.5px] hover:translate-y-[0.5px]"
|
||||
title="Hapus Data"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
{editRecord && (
|
||||
<EditHasilStuntingModal
|
||||
isOpen={!!editRecord}
|
||||
onClose={() => setEditRecord(null)}
|
||||
record={editRecord}
|
||||
posyanduList={posyanduList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl border-2 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] w-full max-w-lg overflow-hidden animate-in fade-in zoom-in duration-200">
|
||||
{/* Header */}
|
||||
<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">Panjang</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>
|
||||
<span className="text-center">Tgl Upload</span>
|
||||
<span className="text-center">Aksi</span>
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b-2 border-black bg-black text-white">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="w-5 h-5" />
|
||||
<h3 className="font-black text-lg">Edit Data Pengukuran</h3>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-1.5 rounded-full hover:bg-white/10 transition-colors">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2 text-gray-300">
|
||||
<Activity className="w-10 h-10 opacity-30" />
|
||||
<p className="text-sm text-gray-400 font-semibold">Tidak ada data untuk tahun {selectedYear}</p>
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-6 flex flex-col gap-4 max-h-[80vh] overflow-y-auto">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-1">Panjang/Tinggi Badan (cm)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={tinggi}
|
||||
onChange={e => 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
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-1">Berat Badan (kg)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={berat}
|
||||
onChange={e => 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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((row, idx) => {
|
||||
const isStunting = row.status_stunting === true
|
||||
|
||||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
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`}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-1">Z-Score (SD)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={zScore}
|
||||
onChange={e => 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
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-1">Status Stunting</label>
|
||||
<select
|
||||
value={statusStunting}
|
||||
onChange={e => setStatusStunting(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 bg-white transition-colors"
|
||||
required
|
||||
>
|
||||
{/* Index */}
|
||||
<div className="flex justify-center">
|
||||
<span className="text-xs text-gray-400 font-bold">{idx + 1}</span>
|
||||
</div>
|
||||
<option value="false">Normal</option>
|
||||
<option value="true">Stunting</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Panjang Badan */}
|
||||
<div className="text-center">
|
||||
<span className="font-bold">{row.tinggi_badan ?? '-'}</span>
|
||||
{row.tinggi_badan && <span className="text-xs text-gray-400 ml-0.5">cm</span>}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-1">Posyandu</label>
|
||||
<select
|
||||
value={namaPosyandu}
|
||||
onChange={e => setNamaPosyandu(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 bg-white transition-colors"
|
||||
required
|
||||
>
|
||||
<option value="">Pilih Posyandu</option>
|
||||
{posyanduList.map((p, idx) => (
|
||||
<option key={idx} value={p.nama_posyandu}>
|
||||
{p.nama_posyandu}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-1">Tanggal Pengukuran</label>
|
||||
<input
|
||||
type="date"
|
||||
value={tanggalUpload}
|
||||
onChange={e => 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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Berat Badan */}
|
||||
<div className="text-center">
|
||||
<span className="font-bold">{row.berat_badan ?? '-'}</span>
|
||||
{row.berat_badan && <span className="text-xs text-gray-400 ml-0.5">kg</span>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-1">Rekomendasi / Pesan AI</label>
|
||||
<textarea
|
||||
value={pesanAi}
|
||||
onChange={e => setPesanAi(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 resize-none transition-colors"
|
||||
rows={4}
|
||||
placeholder="Tulis pesan atau rekomendasi..."
|
||||
/>
|
||||
</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 ? (
|
||||
<span className="text-xs text-gray-300">—</span>
|
||||
) : (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold border ${isStunting
|
||||
? 'bg-red-50 border-red-200 text-red-700'
|
||||
: 'bg-emerald-50 border-emerald-200 text-emerald-700'
|
||||
}`}>
|
||||
{isStunting ? '⚠ Stunting' : '✓ Normal'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pesan AI — truncated */}
|
||||
<div className="pr-2">
|
||||
{row.pesan_ai ? (
|
||||
<p className="text-xs text-gray-600 leading-relaxed">
|
||||
{row.pesan_ai.length > 80
|
||||
? row.pesan_ai.slice(0, 80) + '..........'
|
||||
: row.pesan_ai}
|
||||
</p>
|
||||
) : (
|
||||
<span className="text-xs text-gray-300 italic">Tidak ada pesan</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nama Posyandu */}
|
||||
<div className="text-center">
|
||||
<span className="text-xs text-gray-600">{row.nama_posyandu ?? '-'}</span>
|
||||
</div>
|
||||
|
||||
{/* Tanggal Upload */}
|
||||
<div className="text-center">
|
||||
<span className="text-xs text-gray-500">{formatDate(row.tanggal_upload)}</span>
|
||||
</div>
|
||||
|
||||
{/* Aksi: Cetak PDF */}
|
||||
<div className="flex justify-center">
|
||||
<CetakPDFButton row={row} allData={data} pengguna={pengguna} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 py-3 border-2 border-black font-bold text-sm rounded-xl hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Batal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 py-3 bg-black text-white font-black text-sm rounded-xl hover:bg-gray-800 transition-all flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Simpan Perubahan'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,12 @@ export default async function DetailPenggunaKelolaPage({ params }: Props) {
|
|||
.eq('id_balita', pengguna.id)
|
||||
.order('tanggal_upload', { ascending: false })
|
||||
|
||||
// Fetch list posyandu for editing options
|
||||
const { data: posyanduList } = await supabase
|
||||
.from('detail_posyandu')
|
||||
.select('nama_posyandu')
|
||||
.order('nama_posyandu', { ascending: true })
|
||||
|
||||
const formatDate = (d: string | null) => {
|
||||
if (!d) return null
|
||||
return new Date(d).toLocaleDateString('id-ID', {
|
||||
|
|
@ -187,7 +193,7 @@ export default async function DetailPenggunaKelolaPage({ params }: Props) {
|
|||
</div>
|
||||
|
||||
{/* Tabel Riwayat Hasil Stunting */}
|
||||
<HasilStuntingTable data={hasilData ?? []} pengguna={pengguna} />
|
||||
<HasilStuntingTable data={hasilData ?? []} pengguna={pengguna} posyanduList={posyanduList ?? []} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
'use client'
|
||||
|
||||
import { useActionState, useEffect, useState } from 'react'
|
||||
import { updateAkunBalita } from '@/app/actions'
|
||||
import { User, MapPin, Phone, Baby, Calendar, Lock, AtSign, CheckCircle, XCircle, X } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { updateAkunBalita, deleteAkunBalita } from '@/app/actions'
|
||||
import { User, MapPin, Phone, Baby, Calendar, Lock, AtSign, Trash2 } from 'lucide-react'
|
||||
|
||||
interface AkunBalita {
|
||||
id: string
|
||||
|
|
@ -22,7 +23,33 @@ interface Props {
|
|||
import { showSwal } from '@/lib/swal'
|
||||
|
||||
export function EditPenggunaForm({ pengguna }: Props) {
|
||||
const router = useRouter()
|
||||
const [state, formAction, isPending] = useActionState(updateAkunBalita, null)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirmed = await showSwal.confirm(
|
||||
'Hapus Akun Pengguna?',
|
||||
'Tindakan ini akan menghapus akun balita dan seluruh riwayat pemeriksaan stunting terkait secara permanen!'
|
||||
)
|
||||
|
||||
if (confirmed.isConfirmed) {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
const res = await deleteAkunBalita(pengguna.id)
|
||||
if (res.success) {
|
||||
await showSwal.success('Berhasil!', res.message)
|
||||
router.push('/dashboard/manajemen-akun/pengguna')
|
||||
} else {
|
||||
showSwal.error('Gagal!', res.message)
|
||||
}
|
||||
} catch (err: any) {
|
||||
showSwal.error('Gagal!', err.message || 'Terjadi kesalahan.')
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (state) {
|
||||
|
|
@ -117,15 +144,24 @@ export function EditPenggunaForm({ pengguna }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<div className="pt-4">
|
||||
{/* Submit & Delete Actions */}
|
||||
<div className="pt-4 flex flex-col sm:flex-row gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full bg-black text-white font-bold py-4 rounded-lg hover:bg-gray-800 transition-all shadow-[4px_4px_0px_0px_rgba(0,0,0,0.2)] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,0.2)] hover:translate-x-[2px] hover:translate-y-[2px] disabled:opacity-60 disabled:cursor-not-allowed disabled:transform-none"
|
||||
disabled={isPending || isDeleting}
|
||||
className="flex-1 bg-black text-white font-bold py-4 rounded-lg hover:bg-gray-800 transition-all shadow-[4px_4px_0px_0px_rgba(0,0,0,0.2)] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,0.2)] hover:translate-x-[2px] hover:translate-y-[2px] disabled:opacity-60 disabled:cursor-not-allowed disabled:transform-none"
|
||||
>
|
||||
{isPending ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={isPending || isDeleting}
|
||||
className="bg-red-600 text-white font-bold py-4 px-6 rounded-lg hover:bg-red-700 transition-all shadow-[4px_4px_0px_0px_rgba(220,38,38,0.3)] hover:shadow-[2px_2px_0px_0px_rgba(220,38,38,0.3)] hover:translate-x-[2px] hover:translate-y-[2px] disabled:opacity-60 disabled:cursor-not-allowed disabled:transform-none flex items-center justify-center gap-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
{isDeleting ? 'Menghapus...' : 'Hapus Akun'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Reference in New Issue