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

204 lines
9.8 KiB
TypeScript

'use client'
import { useActionState, useEffect, useState, useCallback, useRef } from 'react'
import { X, Calendar, Clock, Loader2, Save, AlertCircle, CheckCircle2 } from 'lucide-react'
import { updateJadwal, getOccupiedSlots } from './action-jadwal'
import { showSwal } from '@/lib/swal'
interface JadwalData {
id: string
posyandu_id: string
tanggal: string
jam_mulai: string
jam_selesai: string
detail_posyandu: {
nama_posyandu: string
}
}
interface Props {
isOpen: boolean
onClose: () => void
data: JadwalData | null
adminName: string
}
const SESSIONS = [
{ start: '08:00', end: '10:00' },
{ start: '11:00', end: '13:00' },
{ start: '14:00', end: '16:00' },
]
export function JadwalFormModal({ isOpen, onClose, data, adminName }: Props) {
const [state, formAction, isPending] = useActionState(updateJadwal, null)
const [selectedDate, setSelectedDate] = useState('')
const [occupiedSlots, setOccupiedSlots] = useState<{ start: string; end: string; posyandu_id: string }[]>([])
const [isLoadingSlots, setIsLoadingSlots] = useState(false)
const [selectedSession, setSelectedSession] = useState<{ start: string; end: string } | null>(null)
const processedStateRef = useRef<any>(null)
const fetchSlots = useCallback(async (date: string) => {
setIsLoadingSlots(true)
const res = await getOccupiedSlots(date)
if (res.success) {
setOccupiedSlots(res.slots)
}
setIsLoadingSlots(false)
}, [])
useEffect(() => {
if (data && isOpen) {
setSelectedDate(data.tanggal)
setSelectedSession({
start: data.jam_mulai.slice(0, 5),
end: data.jam_selesai.slice(0, 5)
})
fetchSlots(data.tanggal)
}
}, [data, isOpen, fetchSlots])
useEffect(() => {
if (state && state !== processedStateRef.current) {
onClose()
if (state.success) {
showSwal.success('Berhasil!', state.message)
} else {
showSwal.error('Gagal!', state.message)
}
processedStateRef.current = state
}
}, [state, onClose])
useEffect(() => {
if (isOpen) {
processedStateRef.current = null
}
}, [isOpen])
if (!isOpen || !data) return null
const allSlotsFull = SESSIONS.every(session =>
occupiedSlots.some(occ =>
occ.start === session.start &&
occ.end === session.end &&
occ.posyandu_id !== data.posyandu_id
)
)
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm">
<div className="bg-white w-full max-w-md rounded-2xl border-2 border-black shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] overflow-hidden animate-in zoom-in duration-200">
{/* Header */}
<div className="p-5 bg-black border-b-2 border-black flex justify-between items-center text-white">
<div className="flex flex-col">
<h3 className="text-base font-black uppercase tracking-tight">Atur Ulang Jadwal</h3>
<p className="text-[9px] text-gray-400 font-bold uppercase tracking-widest">{data.detail_posyandu.nama_posyandu}</p>
</div>
<button onClick={onClose} className="p-1.5 hover:bg-white/10 rounded-full transition-all">
<X className="w-5 h-5" />
</button>
</div>
<form action={formAction} className="p-6 flex flex-col gap-6">
<input type="hidden" name="id" value={data.id} />
<input type="hidden" name="edited_by" value={adminName} />
<input type="hidden" name="jam_mulai" value={selectedSession?.start || ''} />
<input type="hidden" name="jam_selesai" value={selectedSession?.end || ''} />
{/* Tanggal */}
<div className="flex flex-col gap-2">
<label className="text-[9px] font-black uppercase tracking-widest text-gray-400 flex items-center gap-2">
<Calendar className="w-3.5 h-3.5" /> Tanggal Pelaksanaan
</label>
<input
type="date"
name="tanggal"
value={selectedDate}
onChange={(e) => {
setSelectedDate(e.target.value)
fetchSlots(e.target.value)
}}
required
className="w-full p-3 border-2 border-black rounded-xl font-bold text-sm focus:outline-none focus:ring-4 focus:ring-black/5 transition-all"
/>
</div>
{/* Sesi Section */}
<div className="flex flex-col gap-3">
<label className="text-[9px] font-black uppercase tracking-widest text-gray-400 flex items-center gap-2">
<Clock className="w-3.5 h-3.5" /> Pilih Sesi Tersedia
</label>
{allSlotsFull && (
<div className="p-3 bg-red-50 border border-red-200 rounded-xl flex gap-2 items-center">
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0" />
<p className="text-[10px] font-bold text-red-700 uppercase tracking-tight leading-tight">
Maaf, seluruh jadwal di hari ini sudah penuh.
</p>
</div>
)}
<div className="grid grid-cols-1 gap-3">
{SESSIONS.map((session, i) => {
const isOccupied = occupiedSlots.some(occ =>
occ.start === session.start &&
occ.end === session.end &&
occ.posyandu_id !== data.posyandu_id
)
const isSelected = selectedSession?.start === session.start
const isCurrent = data.jam_mulai.slice(0, 5) === session.start && data.tanggal === selectedDate
return (
<button
key={i}
type="button"
disabled={isOccupied || isLoadingSlots}
onClick={() => setSelectedSession(session)}
className={`relative p-3.5 border-2 rounded-xl transition-all flex items-center justify-between group
${isOccupied
? 'bg-gray-50 border-gray-100 opacity-50 cursor-not-allowed'
: isSelected
? 'bg-black border-black text-white shadow-[3px_3px_0px_0px_rgba(0,0,0,0.2)]'
: 'bg-white border-black hover:bg-gray-50'
}
`}
>
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded-lg ${isSelected ? 'bg-white/20' : 'bg-gray-100 group-hover:bg-black group-hover:text-white transition-colors'}`}>
<Clock className="w-4 h-4" />
</div>
<div className="flex flex-col items-start">
<span className="text-base font-black tracking-tight">{session.start} - {session.end}</span>
<span className={`text-[8px] font-bold uppercase tracking-widest ${isSelected ? 'text-gray-400' : 'text-gray-400'}`}>
{isOccupied ? 'Sesi Terpakai' : isCurrent ? 'Jadwal Saat Ini' : 'Sesi Tersedia'}
</span>
</div>
</div>
{isSelected && <CheckCircle2 className="w-5 h-5 text-emerald-400" />}
</button>
)
})}
</div>
</div>
<div className="pt-2">
<button
type="submit"
disabled={isPending || !selectedSession || allSlotsFull}
className="w-full py-4 bg-black text-white font-black text-xs uppercase tracking-widest rounded-xl hover:bg-gray-800 transition-all shadow-[4px_4px_0px_0px_rgba(0,0,0,0.3)] hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] flex items-center justify-center gap-2 disabled:opacity-50 disabled:grayscale"
>
{isPending ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Save className="w-5 h-5" />
)}
{isPending ? 'Menyimpan...' : 'Update Jadwal'}
</button>
</div>
</form>
</div>
</div>
)
}