333 lines
18 KiB
TypeScript
333 lines
18 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={`overflow-hidden rounded-2xl border-2 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] transition-colors ${showHistory ? 'bg-slate-50' : 'bg-white'}`}>
|
|
<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>
|
|
|
|
<InstantScheduleModal
|
|
isOpen={isInstantModalOpen}
|
|
onClose={() => setIsInstantModalOpen(false)}
|
|
adminName={userName}
|
|
/>
|
|
|
|
<JadwalFormModal
|
|
isOpen={isEditModalOpen}
|
|
onClose={() => setIsEditModalOpen(false)}
|
|
data={selectedJadwal}
|
|
adminName={userName}
|
|
/>
|
|
</div >
|
|
)
|
|
}
|