204 lines
9.8 KiB
TypeScript
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>
|
|
)
|
|
}
|