'use client' import { useState, useRef, useEffect } from 'react' import { FileDown, FolderOpen, X, Loader2, CheckCircle2, ChevronDown, Calendar, Printer } from 'lucide-react' import { supabase } from '@/lib/supabase' import { showSwal } from '@/lib/swal' import { getPetugasLokalByPosyandu } from './action-jadwal' // ─── TYPES ──────────────────────────────────────────────────────── interface JadwalItem { id: string tanggal: string jam_mulai: string jam_selesai: string diedit_oleh: string posyandu_id: string detail_posyandu: { nama_posyandu: string alamat: string } } interface PetugasLokal { nama_petugas: string nomor_hp: string jabatan: string } const MONTHS = [ { num: 1, name: 'Januari' }, { num: 2, name: 'Februari' }, { num: 3, name: 'Maret' }, { num: 4, name: 'April' }, { num: 5, name: 'Mei' }, { num: 6, name: 'Juni' }, { num: 7, name: 'Juli' }, { num: 8, name: 'Agustus' }, { num: 9, name: 'September' }, { num: 10, name: 'Oktober' }, { num: 11, name: 'November' }, { num: 12, name: 'Desember' }, ] const START_YEAR = 2026 const currentYear = new Date().getFullYear() const YEARS = Array.from( { length: Math.max(currentYear, START_YEAR) - START_YEAR + 1 }, (_, i) => START_YEAR + i, ) type Step = 'config' | 'generating' | 'done' | 'error' interface Props { adminName: string } export function CetakBatchJadwalModal({ adminName }: Props) { const [open, setOpen] = useState(false) const [year, setYear] = useState(START_YEAR) const [month, setMonth] = useState(new Date().getMonth() + 1) const [dirHandle, setDirHandle] = useState(null) const [step, setStep] = useState('config') const [progress, setProgress] = useState({ current: 0, total: 0, name: '' }) const [elapsed, setElapsed] = useState(0) const [errorMsg, setErrorMsg] = useState('') // Template state for batch processing const [activeJadwal, setActiveJadwal] = useState(null) const [activePetugas, setActivePetugas] = useState([]) const templateRef = useRef(null) const monthName = MONTHS.find(m => m.num === month)?.name ?? 'Bulan' const folderName = `jadwal_${monthName.toLowerCase()}_${year}` const supportsFileSys = typeof window !== 'undefined' && 'showDirectoryPicker' in window const pickDir = async () => { try { const handle = await (window as any).showDirectoryPicker({ mode: 'readwrite' }) setDirHandle(handle) } catch { /* user cancelled */ } } const reset = () => { setStep('config') setProgress({ current: 0, total: 0, name: '' }) setElapsed(0) setErrorMsg('') setActiveJadwal(null) setActivePetugas([]) } const handleClose = () => { setOpen(false) setTimeout(reset, 300) } const handleGenerate = async () => { setStep('generating') const startTime = Date.now() const timer = setInterval(() => setElapsed(Math.floor((Date.now() - startTime) / 1000)), 500) try { const { default: html2canvas } = await import('html2canvas') const { default: jsPDF } = await import('jspdf') // 1. Fetch schedules for the period const startOfMonth = `${year}-${String(month).padStart(2, '0')}-01` const endOfMonth = new Date(year, month, 0).toISOString().split('T')[0] const { data: schedules, error: eJ } = await supabase .from('jadwal_posyandu') .select(` id, tanggal, jam_mulai, jam_selesai, diedit_oleh, posyandu_id, detail_posyandu ( nama_posyandu, alamat ) `) .gte('tanggal', startOfMonth) .lte('tanggal', endOfMonth) .order('tanggal', { ascending: true }) if (eJ) throw new Error(eJ.message) if (!schedules || schedules.length === 0) { setErrorMsg(`Tidak ada jadwal ditemukan untuk ${monthName} ${year}`) setStep('error') clearInterval(timer) return } setProgress({ current: 0, total: schedules.length, name: '' }) // 2. Prepare folder let folderHandle: FileSystemDirectoryHandle | null = null if (dirHandle) { folderHandle = await dirHandle.getDirectoryHandle(folderName, { create: true }) } // 3. Loop and Generate for (let i = 0; i < schedules.length; i++) { const j = schedules[i] as any as JadwalItem setProgress({ current: i + 1, total: schedules.length, name: j.detail_posyandu.nama_posyandu }) // Fetch local officers for this Posyandu const resP = await getPetugasLokalByPosyandu(j.posyandu_id) const petugas = resP.success ? resP.petugas : [] // Update template and wait for render setActiveJadwal(j) setActivePetugas(petugas || []) await new Promise(r => setTimeout(r, 400)) // Stable render buffer if (!templateRef.current) continue // Capture const canvas = await html2canvas(templateRef.current, { scale: 2, useCORS: true, backgroundColor: '#ffffff', logging: false, }) const imgData = canvas.toDataURL('image/png') const pdf = new jsPDF('p', 'mm', 'a4') const pageW = pdf.internal.pageSize.getWidth() const imgH = (canvas.height * pageW) / canvas.width pdf.addImage(imgData, 'PNG', 0, 0, pageW, imgH) const fileName = `Jadwal_${j.detail_posyandu.nama_posyandu.replace(/\s+/g, '_')}_${j.tanggal}.pdf` const blob = pdf.output('blob') if (folderHandle) { const fh = await folderHandle.getFileHandle(fileName, { create: true }) const writable = await fh.createWritable() await writable.write(blob) await writable.close() } else { const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = fileName a.click() URL.revokeObjectURL(url) await new Promise(r => setTimeout(r, 200)) } } clearInterval(timer) setStep('done') await showSwal.success('Selesai!', `Berhasil mencetak ${schedules.length} file PDF jadwal.`) handleClose() } catch (err: any) { clearInterval(timer) setErrorMsg(err?.message ?? 'Gagal memproses batch PDF.') setStep('error') } finally { setActiveJadwal(null) setActivePetugas([]) } } const pct = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0 const estRemaining = progress.current > 0 ? Math.round((elapsed / progress.current) * (progress.total - progress.current)) : null // ── Template helper variables ────────────────────────────────── const now = new Date() const tglCetak = now.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' }) const jamCetak = now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }) return ( <> {open && (
e.stopPropagation()}> {/* Header */}

Cetak Batch Jadwal

Generate PDF Kolektif Per Periode

{step !== 'generating' && ( )}
{step === 'config' && (

Lokasi Penyimpanan

{supportsFileSys ? ( ) : (
⚠ Browser Anda tidak mendukung akses folder. File akan diunduh satu per satu secara otomatis.
)}
)} {step === 'generating' && (

Sedang Memproses PDF...

Jangan tutup halaman ini

{progress.current} / {progress.total} POSYANDU {pct}%
{progress.name && (

Memproses:

{progress.name}

)}

Berjalan

{String(Math.floor(elapsed / 60)).padStart(2, '0')}:{String(elapsed % 60).padStart(2, '0')}

Estimasi

{estRemaining !== null ? `${String(Math.floor(estRemaining / 60)).padStart(2, '0')}:${String(estRemaining % 60).padStart(2, '0')}` : '--:--'}

)} {step === 'error' && (

Gagal Memproses

{errorMsg}

)}
)} {/* ─── Hidden PDF Template (Same as CetakPDFJadwal) ─── */} {activeJadwal && (
Pemerintah Kabupaten / Kota
DINAS KESEHATAN & POSYANDU
Nomor Dokumen
JDWL-{activeJadwal.id.slice(0, 8).toUpperCase()}

SURAT PEMBERITAHUAN JADWAL PELAKSANAAN

DETAIL POSYANDU
Nama Posyandu
{activeJadwal.detail_posyandu.nama_posyandu}
Alamat Lokasi
{activeJadwal.detail_posyandu.alamat}
WAKTU PELAKSANAAN
Hari & Tanggal
{new Date(activeJadwal.tanggal).toLocaleDateString('id-ID', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}
Sesi Operasional
{activeJadwal.jam_mulai.slice(0, 5)} - {activeJadwal.jam_selesai.slice(0, 5)} WIB
DAFTAR PETUGAS LOKAL BERTUGAS
{activePetugas.length > 0 ? activePetugas.map((p, i) => ( )) : ( )}
Nama Petugas Nomor Telepon Jabatan
{p.nama_petugas} {p.nomor_hp} {p.jabatan}
Belum ada petugas lokal yang terdaftar.
⚠️ CATATAN SISTEM

"Dimohon agar aktivitas program pengecekan stunting di setiap Posyandu dijalankan sesuai dengan waktu dan sesi yang tertera dalam jadwal, guna mendukung pemeliharaan sistem yang baik."

Metode Penjadwalan:
Sistem Penjadwalan Otomatis (Algoritma Random Batch)
Ditentukan oleh Admin: {activeJadwal.diedit_oleh.replace('[HISTORY] ', '')}
Detail Pengunduhan:
Dicetak oleh Admin: {adminName}
Waktu Cetak: {tglCetak}, {jamCetak} WIB
DOKUMEN INI DIHASILKAN SECARA OTOMATIS OLEH SISTEM CLOUD STUNTING
)} ) }