TKK_E32231405/app/dashboard/kelola-jadwal/JadwalTable.tsx

337 lines
19 KiB
TypeScript

'use client'
import { useState, useMemo, useActionState, useEffect, useRef } from 'react'
import { Search, Calendar, Clock, Trash2, Building2, Sparkles, Filter, Edit3, Loader2, History, RotateCcw, Printer } from 'lucide-react'
import { InstantScheduleModal } from './InstantScheduleModal'
import { JadwalFormModal } from './JadwalFormModal'
import { CetakPDFJadwal } from './CetakPDFJadwal'
import { CetakBatchJadwalModal } from './CetakBatchJadwalModal'
import { deleteSchedulesByDate, deleteJadwal, archiveSchedulesByMonth, deleteAllHistory } from './action-jadwal'
import { showSwal } from '@/lib/swal'
interface JadwalWithPosyandu {
id: string
posyandu_id: string
tanggal: string
jam_mulai: string
jam_selesai: string
diedit_oleh: string
detail_posyandu: {
nama_posyandu: string
alamat: string
}
}
interface Props {
data: JadwalWithPosyandu[]
userName: string
}
export function JadwalTable({ data, userName }: Props) {
const [searchTerm, setSearchTerm] = useState('')
const [filterDate, setFilterDate] = useState('')
const [showHistory, setShowHistory] = useState(false)
const [isInstantModalOpen, setIsInstantModalOpen] = useState(false)
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [selectedJadwal, setSelectedJadwal] = useState<JadwalWithPosyandu | null>(null)
const [isDeleting, setIsDeleting] = useState<string | null>(null)
const now = new Date()
const currentMonth = now.getMonth()
const currentYear = now.getFullYear()
const filteredData = useMemo(() => {
return data.filter(j => {
const isHistoryRecord = j.diedit_oleh.startsWith('[HISTORY]')
// History View Logic
if (showHistory) {
if (!isHistoryRecord) return false
} else {
if (isHistoryRecord) return false
}
const dateObj = new Date(j.tanggal)
const isCurrentMonth = dateObj.getMonth() === currentMonth && dateObj.getFullYear() === currentYear
// Monthly Filter Logic for Non-History
if (!showHistory && !isCurrentMonth) return false
const searchTermClean = searchTerm.toLowerCase()
const matchesSearch = j.detail_posyandu.nama_posyandu.toLowerCase().includes(searchTermClean) ||
j.diedit_oleh.toLowerCase().includes(searchTermClean)
const matchesDate = filterDate ? j.tanggal === filterDate : true
return matchesSearch && matchesDate
})
}, [data, searchTerm, filterDate, showHistory, currentMonth, currentYear])
const isScheduledThisMonth = useMemo(() => {
return data.some(j => {
const dateObj = new Date(j.tanggal)
const isCurrentMonth = dateObj.getMonth() === currentMonth && dateObj.getFullYear() === currentYear
return isCurrentMonth && !j.diedit_oleh.startsWith('[HISTORY]')
})
}, [data, currentMonth, currentYear])
const handleDeleteDate = async () => {
if (!filterDate) {
showSwal.error('Pilih Tanggal', 'Silakan pilih tanggal terlebih dahulu untuk menghapus jadwal pada hari tersebut.')
return
}
const confirm = await showSwal.confirm(
'Hapus Jadwal Hari Ini?',
`Seluruh jadwal untuk tanggal ${new Date(filterDate).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' })} akan dihapus.`
)
if (confirm.isConfirmed) {
const res = await deleteSchedulesByDate(filterDate)
if (res.success) showSwal.success('Berhasil!', res.message)
else showSwal.error('Gagal!', res.message)
}
}
const handleResetMonth = async () => {
const confirm = await showSwal.confirm(
'Arsipkan Jadwal Bulan Ini?',
'Jadwal bulan ini akan dipindahkan ke Histori dan disembunyikan dari tabel utama. Anda bisa melihatnya kembali di fitur Lihat Histori.'
)
if (confirm.isConfirmed) {
const res = await archiveSchedulesByMonth(currentMonth, currentYear)
if (res.success) showSwal.success('Berhasil!', res.message)
else showSwal.error('Gagal!', res.message)
}
}
const handleClearHistory = async () => {
const confirm = await showSwal.confirm(
'Hapus Seluruh Histori?',
'Tindakan ini akan menghapus SELURUH data di riwayat penjadwalan secara PERMANEN. Data tidak dapat dipulihkan.'
)
if (confirm.isConfirmed) {
const res = await deleteAllHistory()
if (res.success) showSwal.success('Berhasil!', res.message)
else showSwal.error('Gagal!', res.message)
}
}
const handleDeleteJadwal = async (id: string, name: string) => {
const confirm = await showSwal.confirm(
'Hapus Jadwal?',
`Apakah Anda yakin ingin menghapus jadwal untuk ${name}?`
)
if (confirm.isConfirmed) {
setIsDeleting(id)
const res = await deleteJadwal(id)
if (res.success) showSwal.success('Berhasil!', res.message)
else showSwal.error('Gagal!', res.message)
setIsDeleting(null)
}
}
const handleEditJadwal = (jadwal: JadwalWithPosyandu) => {
setSelectedJadwal(jadwal)
setIsEditModalOpen(true)
}
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"]
return (
<div className="flex flex-col gap-6">
{/* Header / Action Bar */}
<div className="flex flex-col xl:flex-row justify-between items-start xl:items-center gap-4">
<div className="flex flex-col md:flex-row gap-3 w-full xl:w-auto">
{/* Search */}
<div className="relative w-full md:w-72">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Cari posyandu..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border-2 border-transparent focus:border-black rounded-xl text-xs font-bold outline-none transition-all"
/>
</div>
{/* Date Filter */}
<div className="relative w-full md:w-48">
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="date"
value={filterDate}
onChange={(e) => setFilterDate(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border-2 border-transparent focus:border-black rounded-xl text-xs font-bold outline-none transition-all"
/>
</div>
<button
onClick={() => setShowHistory(!showHistory)}
className={`flex items-center justify-center gap-2 px-5 py-2.5 border-2 rounded-xl text-xs font-black transition-all
${showHistory
? 'bg-black text-white border-black'
: 'bg-white text-gray-600 border-gray-100 hover:border-black hover:text-black'
}`}
>
<History className="w-4 h-4" />
{showHistory ? 'Sembunyikan Histori' : 'Lihat Histori'}
</button>
</div>
<div className="flex flex-wrap md:flex-nowrap items-center gap-3 w-full md:w-auto">
<CetakBatchJadwalModal adminName={userName} />
{showHistory ? (
<button
onClick={handleClearHistory}
disabled={filteredData.length === 0}
className="flex-1 md:flex-none flex items-center justify-center gap-2 px-5 py-2.5 bg-white text-purple-600 border-2 border-purple-100 font-black rounded-xl hover:bg-purple-50 hover:border-purple-500 transition-all text-xs disabled:opacity-50 disabled:grayscale"
>
<Trash2 className="w-4 h-4" />
Hapus Semua Histori
</button>
) : (
<>
{isScheduledThisMonth && (
<button
onClick={handleResetMonth}
className="flex-1 md:flex-none flex items-center justify-center gap-2 px-5 py-2.5 bg-white text-red-600 border-2 border-red-100 font-black rounded-xl hover:bg-red-50 hover:border-red-500 transition-all text-xs"
title="Reset Seluruh Jadwal Bulan Ini"
>
<RotateCcw className="w-4 h-4" />
Reset {monthNames[currentMonth]}
</button>
)}
<button
onClick={() => setIsInstantModalOpen(true)}
disabled={isScheduledThisMonth}
className={`flex-1 md:flex-none flex items-center justify-center gap-2 px-5 py-2.5 font-black rounded-xl transition-all shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] text-xs uppercase tracking-widest
${isScheduledThisMonth
? 'bg-gray-100 text-gray-400 cursor-not-allowed shadow-none translate-x-0 translate-y-0 opacity-50 grayscale'
: 'bg-red-600 text-white hover:bg-red-700'
}`}
>
<Sparkles className="w-4 h-4" />
{isScheduledThisMonth ? 'Terjadwal' : 'Penjadwalan Instan'}
</button>
</>
)}
</div>
</div>
{/* Table */}
<div className={`rounded-2xl border-2 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] transition-colors ${showHistory ? 'bg-slate-50' : 'bg-white'} overflow-hidden`}>
<div className="overflow-x-auto">
<div className="min-w-[1000px] md:min-w-full">
<table className="w-full text-left border-collapse">
<thead className={`${showHistory ? 'bg-purple-600' : 'bg-black'} text-white transition-colors`}>
<tr>
<th className="px-6 py-5 text-[10px] font-black uppercase tracking-widest text-center w-20">No</th>
<th className="px-6 py-5 text-[10px] font-black uppercase tracking-widest">Waktu & Sesi</th>
<th className="px-6 py-5 text-[10px] font-black uppercase tracking-widest">Detail Posyandu</th>
<th className="px-6 py-5 text-[10px] font-black uppercase tracking-widest">Oleh Admin</th>
<th className="px-6 py-5 text-[10px] font-black uppercase tracking-widest text-center">Aksi</th>
</tr>
</thead>
<tbody className={`${showHistory ? 'bg-slate-50 divide-purple-100' : 'bg-white divide-gray-100'} divide-y-2`}>
{filteredData.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-24 text-center">
<div className="flex flex-col items-center gap-4 text-gray-300">
<div className="p-4 bg-gray-50 rounded-full">
<Calendar className="w-12 h-12 opacity-20" />
</div>
<div className="flex flex-col gap-1">
<p className="font-black text-gray-400">Belum ada jadwal ditemukan</p>
<p className="text-xs font-semibold">Gunakan Penjadwalan Instan untuk membuat jadwal otomatis.</p>
</div>
</div>
</td>
</tr>
) : (
filteredData.map((j, idx) => (
<tr key={j.id} className={`${showHistory ? 'hover:bg-purple-100/50' : 'hover:bg-red-50/30'} transition-colors group`}>
<td className={`px-6 py-6 text-center font-black ${showHistory ? 'text-purple-300' : 'text-gray-300'} group-hover:text-red-300 text-lg`}>
{idx + 1}
</td>
<td className="px-6 py-6">
<div className="flex flex-col gap-1">
<div className={`flex items-center gap-2 ${showHistory ? 'text-purple-600' : 'text-red-600'} font-black`}>
<Clock className="w-4 h-4" />
<span className="text-lg">{j.jam_mulai.slice(0, 5)} - {j.jam_selesai.slice(0, 5)}</span>
</div>
<span className="text-[10px] font-bold uppercase text-gray-400 tracking-widest">
{new Date(j.tanggal).toLocaleDateString('id-ID', { weekday: 'long', day: 'numeric', month: 'long' })}
</span>
</div>
</td>
<td className="px-6 py-6">
<div className="flex flex-col">
<span className={`font-black text-base ${showHistory ? 'text-purple-900' : 'text-gray-900'} group-hover:text-red-600 transition-colors uppercase tracking-tight`}>
{j.detail_posyandu.nama_posyandu}
</span>
<div className="flex items-center gap-2 text-xs text-gray-500 font-semibold mt-1">
<Building2 className="w-3.5 h-3.5" />
<span className="line-clamp-1">{j.detail_posyandu.alamat}</span>
</div>
</div>
</td>
<td className="px-6 py-6">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full ${showHistory ? 'bg-purple-100' : 'bg-gray-100'} border-2 border-white shadow-sm flex items-center justify-center text-[10px] font-black group-hover:bg-red-100 transition-colors`}>
{j.diedit_oleh.replace('[HISTORY] ', '').charAt(0).toUpperCase()}
</div>
<span className="text-xs font-bold text-gray-700">{j.diedit_oleh.replace('[HISTORY] ', '')}</span>
</div>
</td>
<td className="px-6 py-6">
<div className="flex items-center justify-center gap-3">
<CetakPDFJadwal jadwal={j} currentAdmin={userName} />
<button
onClick={() => handleEditJadwal(j)}
className="p-2 text-gray-400 hover:text-black hover:bg-gray-100 rounded-lg transition-all"
title="Edit"
>
<Edit3 className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteJadwal(j.id, j.detail_posyandu.nama_posyandu)}
disabled={isDeleting === j.id}
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all disabled:opacity-50"
title="Hapus"
>
{isDeleting === j.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
<InstantScheduleModal
isOpen={isInstantModalOpen}
onClose={() => setIsInstantModalOpen(false)}
adminName={userName}
/>
<JadwalFormModal
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
data={selectedJadwal}
adminName={userName}
/>
</div >
)
}