import React, { useState, useEffect, FormEvent, useMemo } from 'react'; import ReactDOM from 'react-dom/client'; // ================================================================= // PENTING: Ganti dengan URL Web App dari Google Apps Script Anda // ================================================================= const SCRIPT_URL = "https://script.google.com/macros/s/AKfycbwMeQnZXwHszl0qdb2z_9egd69eVipRXCk97CAUt9c75j40Kzr2MMLNp3OV5bdRYLA/exec"; // ================================================================= // Tipe data interface User { name: string; email: string; role: 'user' | 'admin'; } interface SewaData { ID: string; Status_Pembayaran?: string; Jenis_Kegiatan?: string; Tempat?: string; Nama_Pengguna?: string; Tanggal_Pelaksanaan?: string; Tanggal_Berakhir?: string; Total_Tagihan?: number; Surat_Permohonan_Link?: string; Bukti_Bayar_Link?: string; Biaya_Tenda?: number; Biaya_Listrik?: number; [key: string]: any; } // Komponen Ikon const Icon = ({ name, className = '' }: { name: string; className?: string }) => ( {name} ); // Helper untuk VAPID key function urlBase64ToUint8Array(base64String: string) { const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } // Helper untuk konversi file ke Base64 const fileToBase64 = (file: File): Promise => new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { const result = reader.result as string; resolve(result.split(',')[1]); // Hapus prefix data URL }; reader.onerror = error => reject(error); }); // Komponen utama aplikasi const App: React.FC = () => { const [user, setUser] = useState(null); const [sewaData, setSewaData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [view, setView] = useState<'login' | 'register'>('login'); const [activeScreen, setActiveScreen] = useState<'Dashboard' | 'Sewa Lahan' | 'Biaya'>('Dashboard'); const [isModalOpen, setIsModalOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); // State for editing const [isMenuModalOpen, setIsMenuModalOpen] = useState(false); const [toastMessage, setToastMessage] = useState(null); // Coba ambil data user dari local storage useEffect(() => { const storedUser = localStorage.getItem('user'); if (storedUser) { setUser(JSON.parse(storedUser)); } else { setIsLoading(false); } }, []); // Fetch data sewa ketika user sudah login useEffect(() => { if (user) { fetchData(); } }, [user]); // Toast notification effect useEffect(() => { if (toastMessage) { const timer = setTimeout(() => setToastMessage(null), 3000); return () => clearTimeout(timer); } }, [toastMessage]); const handleError = (err: any) => { const errorMessage = err.message || 'Terjadi kesalahan.'; setError(errorMessage); setIsLoading(false); setToastMessage(`Error: ${errorMessage}`); }; const getFromAction = async (action: string, params: Record = {}) => { if (!SCRIPT_URL.includes('macros/s')) { setError("URL Google Apps Script belum diisi. Edit SCRIPT_URL di index.tsx."); return null; } const url = new URL(SCRIPT_URL); url.searchParams.append('action', action); for (const key in params) { url.searchParams.append(key, params[key]); } const response = await fetch(url.toString()); const result = await response.json(); if (result.success) { return result; } else { throw new Error(result.error || 'Gagal mengambil data.'); } }; const fetchData = async () => { if (!user) return; setIsLoading(true); setError(null); try { const result = await getFromAction('getData', { email: user.email, role: user.role }); if (result) setSewaData(result.data as SewaData[]); } catch (err) { handleError(err); } finally { setIsLoading(false); } }; const postToAction = async (action: string, payload: object) => { if (!SCRIPT_URL.includes('macros/s')) { setError("URL Google Apps Script belum diisi. Edit SCRIPT_URL di index.tsx."); return null; } try { const response = await fetch(SCRIPT_URL, { method: 'POST', headers: { 'Content-Type': 'text/plain;charset=utf-8' }, body: JSON.stringify({ action, payload }), }); const resultText = await response.text(); // Coba parse JSON, jika gagal (misal error HTML dari Google), throw error dengan teks aslinya let result; try { result = JSON.parse(resultText); } catch (e) { // Jika HTML error page dari Google, biasanya panjang. Ambil snippetnya. const cleanError = resultText.replace(/<[^>]*>?/gm, '').substring(0, 200); throw new Error(`Server Error: ${cleanError}...`); } if (!result.success) throw new Error(`Gagal: ${result.error || 'Tidak diketahui'}`); return result; } catch (err: any) { throw new Error(err.message || "Gagal menghubungi server."); } } const handleLogin = async (email: string) => { if (!email) { setError("Email tidak boleh kosong."); return; } setIsLoading(true); setError(null); try { const result = await postToAction('login', { email }); if (result && result.success) { setUser(result.user); localStorage.setItem('user', JSON.stringify(result.user)); } } catch (err) { handleError(err); } finally { setIsLoading(false); } }; const handleRegister = async (name: string, email: string) => { if (!name || !email) { setError("Nama dan Email tidak boleh kosong."); return; } setIsLoading(true); setError(null); try { const result = await postToAction('register', { name, email }); if (result && result.success) { setUser(result.user); localStorage.setItem('user', JSON.stringify(result.user)); } } catch (err) { handleError(err); } finally { setIsLoading(false); } }; const handleFormSubmit = async (e: FormEvent, action: 'create' | 'update') => { e.preventDefault(); if (!user && action === 'create') return; if (!editingItem && action === 'update') return; const form = e.currentTarget; const formData = new FormData(form); const data: { [key: string]: any } = Object.fromEntries(formData.entries()); if (action === 'create') data.Email_Penyewa = user!.email; if (action === 'update') data.ID = editingItem!.ID; // === SAFEGUARD DATA TYPES === // Bersihkan input angka dari karakter non-digit (misal titik ribuan) const cleanInt = (val: any) => { if (!val) return 0; // Hapus semua karakter kecuali angka dan minus const str = String(val).replace(/[^0-9\-]/g, ''); const parsed = parseInt(str, 10); return isNaN(parsed) ? 0 : parsed; }; const parseSafeFloat = (val: any) => { if (!val) return 0; const str = String(val).replace(',', '.'); // Handle koma desimal const parsed = parseFloat(str); return isNaN(parsed) ? 0 : parsed; }; // Pastikan Biaya Tenda dan Listrik dikirim sebagai integer murni tanpa format // Jika checkbox tidak dicentang, input field tidak ada di formData, jadi undefined -> 0 data.Biaya_Tenda = cleanInt(data.Biaya_Tenda); data.Biaya_Listrik = cleanInt(data.Biaya_Listrik); if (data.Panjang) data.Panjang = parseSafeFloat(data.Panjang); if (data.Lebar) data.Lebar = parseSafeFloat(data.Lebar); setIsLoading(true); setError(null); try { // Handle file uploads const suratInput = form.elements.namedItem('Surat_Permohonan') as HTMLInputElement | null; if (suratInput && suratInput.files && suratInput.files[0]) { const file = suratInput.files[0]; data.Surat_Permohonan_PDF = await fileToBase64(file); data.Surat_Permohonan_Name = file.name; } const buktiInput = form.elements.namedItem('Bukti_Bayar') as HTMLInputElement | null; if (buktiInput && buktiInput.files && buktiInput.files[0]) { const file = buktiInput.files[0]; data.Bukti_Bayar_PDF = await fileToBase64(file); data.Bukti_Bayar_Name = file.name; } const result = await postToAction(action, data); if (result && result.success) { setToastMessage(action === 'create' ? 'Pengajuan sewa berhasil dibuat!' : 'Data sewa berhasil diperbarui!'); if (action === 'create' && result.data) { setSewaData(prevData => [result.data, ...prevData]); } else if (action === 'update' && result.data) { setSewaData(prevData => prevData.map(item => item.ID === result.data.ID ? result.data : item)); } else { fetchData(); } form.reset(); setIsModalOpen(false); setEditingItem(null); } } catch (err) { handleError(err); } finally { setIsLoading(false); } }; const handleCreateSewa = (e: FormEvent) => handleFormSubmit(e, 'create'); const handleUpdateSewa = (e: FormEvent) => handleFormSubmit(e, 'update'); const handleLogout = () => { setUser(null); setSewaData([]); localStorage.removeItem('user'); }; // ================================================================= // KOMPONEN UI BARU // ================================================================= const InputField = ({ id, label, type, name, required = false, placeholder = '', ...props }: any) => (
{type === 'date' && }
); const FileInputField = ({ id, label, name, accept, disabled = false, required = false, currentFileUrl }: { id: string; label: string; name: string; accept: string; disabled?: boolean; required?: boolean; currentFileUrl?: string | null; }) => { const [fileName, setFileName] = useState(''); const [isReplacing, setIsReplacing] = useState(false); const handleFileChange = (e: React.ChangeEvent) => { setFileName(e.target.files?.[0]?.name || ''); }; const showInput = !currentFileUrl || isReplacing; return (
{currentFileUrl && !disabled && !isReplacing && ( )} {currentFileUrl && isReplacing && ( )}
{!showInput ? ( ) : (
{fileName || 'Pilih file...'}
)}
) }; const AuthView = () => (

{view === 'login' ? 'Masuk ke Akun' : 'Buat Akun Baru'}

{ e.preventDefault(); const email = (e.currentTarget.elements.namedItem('email') as HTMLInputElement).value; if (view === 'login') { handleLogin(email); } else { const name = (e.currentTarget.elements.namedItem('name') as HTMLInputElement).value; handleRegister(name, email); } }}> {view === 'register' && ( )}

{error &&

{error}

}
); const NotificationBell = () => { const [permission, setPermission] = useState('default'); const [isSubscribing, setIsSubscribing] = useState(false); useEffect(() => { if ('Notification' in window) { setPermission(Notification.permission); } }, []); const subscribeUser = async () => { if (!user || !('serviceWorker' in navigator) || !('PushManager' in window)) return; setIsSubscribing(true); try { const permissionResult = await Notification.requestPermission(); setPermission(permissionResult); if (permissionResult !== 'granted') { setToastMessage('Izin notifikasi tidak diberikan.'); return; } const { publicKey } = await getFromAction('getVapidPublicKey'); if (!publicKey) throw new Error('VAPID public key tidak ditemukan.'); const swRegistration = await navigator.serviceWorker.ready; const subscription = await swRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicKey), }); await postToAction('saveSubscription', { email: user.email, subscription }); setToastMessage('Berhasil mengaktifkan notifikasi!'); } catch (err: any) { console.error('Gagal berlangganan notifikasi:', err); setToastMessage(`Error: ${err.message}`); setPermission('default'); } finally { setIsSubscribing(false); } }; const getIconName = () => { if (permission === 'granted') return 'notifications_active'; if (permission === 'denied') return 'notifications_off'; return 'notifications'; }; const isDisabled = permission === 'denied' || isSubscribing; return ( ); }; const MenuModal = ({ onClose }: { onClose: () => void }) => (
e.stopPropagation()}>

Informasi Kontak

Kontak : 082129164090 (Dian)

); const MainAppView = () => { const closeModal = () => { setIsModalOpen(false); setEditingItem(null); }; return (
setIsMenuModalOpen(true)} />
{activeScreen === 'Dashboard' && } {activeScreen === 'Sewa Lahan' && } {activeScreen === 'Biaya' && }
{isModalOpen && } {isMenuModalOpen && setIsMenuModalOpen(false)} />}
); } const Header = ({ title, onMenuClick }: { title: string; onMenuClick: () => void }) => (

{title}

); const BottomNavBar = () => { const navItems = [ { name: 'Dashboard', icon: 'dashboard' }, { name: 'Sewa Lahan', icon: 'article' }, { name: 'Biaya', icon: 'auto_awesome' }, ]; return ( ); }; const Calendar = ({ events }: { events: SewaData[] }) => { const [currentDate, setCurrentDate] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(null); const daysInMonth = useMemo(() => { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDayOfMonth = new Date(year, month, 1); const lastDateOfMonth = new Date(year, month + 1, 0); const firstDayOfWeek = firstDayOfMonth.getDay(); // 0 for Sunday, 1 for Monday, etc. const lastDate = lastDateOfMonth.getDate(); const dates: (Date | null)[] = Array.from({ length: firstDayOfWeek }, () => null); // padding for (let i = 1; i <= lastDate; i++) dates.push(new Date(year, month, i)); return dates; }, [currentDate]); const changeMonth = (offset: number) => { setCurrentDate(prev => new Date(prev.getFullYear(), prev.getMonth() + offset, 1)); }; const getEventsOnDate = (date: Date) => { if (!date) return []; return events.filter(event => { if (!event.Tanggal_Pelaksanaan || !event.Tanggal_Berakhir) return false; const startDate = new Date(event.Tanggal_Pelaksanaan); startDate.setHours(0,0,0,0); const endDate = new Date(event.Tanggal_Berakhir); endDate.setHours(0,0,0,0); return date >= startDate && date <= endDate; }).sort((a,b) => (a.Jenis_Kegiatan || '').localeCompare(b.Jenis_Kegiatan || '')); } const handleDateClick = (date: Date | null) => { if (!date) return; const eventsOnDate = getEventsOnDate(date); if (eventsOnDate.length > 0) { setSelectedDate(date); } }; const EventModal = ({ date, events: modalEvents, onClose }: { date: Date, events: SewaData[], onClose: () => void }) => (
e.stopPropagation()}>

Kegiatan: {date.toLocaleDateString('id-ID', { day: 'numeric', month: 'long' })}

{modalEvents.length > 0 ? ( modalEvents.map(event => (

{event.Jenis_Kegiatan}

Oleh: {event.Nama_Pengguna}

)) ) : (

Tidak ada kegiatan pada tanggal ini.

)}
); return (

{currentDate.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' })}

{['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'].map(d =>
{d}
)}
{daysInMonth.map((date, i) => { const eventsOnDate = date ? getEventsOnDate(date) : []; const isToday = date && new Date().toDateString() === date.toDateString(); const hasEvents = eventsOnDate.length > 0; return (
handleDateClick(date)} onTouchEnd={(e) => { e.preventDefault(); handleDateClick(date); }} className={`aspect-square flex flex-col p-1 border-t border-l border-gray-200 ${date ? '' : 'bg-gray-50'} ${hasEvents ? 'cursor-pointer hover:bg-red-50 transition-colors' : ''}`}> {date && ( <> {date.getDate()}
{eventsOnDate.slice(0, 2).map(event => (
{event.Jenis_Kegiatan || 'Event'}
))} {eventsOnDate.length > 2 && (
+ {eventsOnDate.length - 2} lainnya
)}
)}
); })}
{selectedDate && ( setSelectedDate(null)} /> )}
); }; const TotalRevenueCard = ({ data, setToastMessage }: { data: SewaData[], setToastMessage: (msg: string) => void }) => { const [selectedYear, setSelectedYear] = useState('all'); const formatCurrency = (amount?: number) => new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(amount || 0); const availableYears = useMemo(() => { const years = new Set(); data.forEach(item => { if (item.Tanggal_Pelaksanaan) { try { const year = new Date(item.Tanggal_Pelaksanaan).getFullYear().toString(); years.add(year); } catch (e) { // ignore invalid dates } } }); return ['all', ...Array.from(years).sort((a, b) => parseInt(b) - parseInt(a))]; }, [data]); const totalRevenue = useMemo(() => { return data .filter(item => { const status = item.Status_Pembayaran?.toLowerCase(); const isPaid = status === 'diterima' || status === 'lunas'; if (!isPaid || !item.Tanggal_Pelaksanaan) return false; if (selectedYear === 'all') return true; try { const itemYear = new Date(item.Tanggal_Pelaksanaan).getFullYear().toString(); return itemYear === selectedYear; } catch(e) { return false; } }) .reduce((sum, item) => sum + (item.Total_Tagihan || 0), 0); }, [data, selectedYear]); const handleCopy = () => { navigator.clipboard.writeText(totalRevenue.toString()); setToastMessage('Total pendapatan disalin!'); }; return (

Total Pendapatan

{formatCurrency(totalRevenue)}

); }; const RevenueChart = ({ data }: { data: SewaData[] }) => { const formatCurrency = (amount?: number) => new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(amount || 0); const chartData = useMemo(() => { const revenueByMonth: { [key: string]: { total: number; date: Date } } = {}; const monthNames = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"]; data.forEach(item => { const status = item.Status_Pembayaran?.toLowerCase(); if ((status === 'diterima' || status === 'lunas') && item.Tanggal_Pelaksanaan && item.Total_Tagihan) { const date = new Date(item.Tanggal_Pelaksanaan); const monthKey = `${monthNames[date.getMonth()]} '${date.getFullYear().toString().slice(-2)}`; if (!revenueByMonth[monthKey]) { revenueByMonth[monthKey] = { total: 0, date: new Date(date.getFullYear(), date.getMonth(), 1) }; } revenueByMonth[monthKey].total += item.Total_Tagihan; } }); const today = new Date(); for (let i = 5; i >= 0; i--) { const d = new Date(today.getFullYear(), today.getMonth() - i, 1); const key = `${monthNames[d.getMonth()]} '${d.getFullYear().toString().slice(-2)}`; if(!revenueByMonth[key]) { revenueByMonth[key] = { total: 0, date: d }; } } return Object.entries(revenueByMonth) .map(([month, data]) => ({ month, total: data.total, date: data.date })) .sort((a, b) => a.date.getTime() - b.date.getTime()) .slice(-6); }, [data]); if (chartData.every(d => d.total === 0)) { return (

Pendapatan

Belum ada data pendapatan yang tercatat.

); } const maxRevenue = Math.max(...chartData.map(d => d.total), 1); const chartHeight = 150; return (

Pendapatan per Bulan

{chartData.map(({ month, total }) => { const barHeight = total > 0 ? (total / maxRevenue) * chartHeight : 0; return (
{total > 100000 ? `${(total / 1000000).toFixed(1).replace('.0','')}jt` : ''}
{month}
); })}
); }; const DashboardScreen = () => { const procedures = [ { icon: 'visibility', text: "Pantau status pengajuan Anda di menu 'Sewa Lahan'." }, { icon: 'payments', text: "Lakukan pembayaran setelah status pengajuan 'Diterima'." }, { icon: 'event_repeat', text: "Jadwal dapat berubah jika berbenturan dengan acara universitas." }, { icon: 'notification_important', text: "Informasi penjadwalan ulang akan diberikan maksimal H-3 acara." }, ]; return (

PROSEDUR BAZZAR

{procedures.map((item, index) => (

{item.text}

))}
{user?.role === 'admin' && ( <> )}
); }; interface SewaCardProps { item: SewaData; onEditClick: () => void; } const SewaCard: React.FC = ({ item, onEditClick }) => { const getStatusChip = (status?: string) => { const s = status?.toLowerCase(); if (s === 'lunas') return 'text-white bg-green-600'; if (s === 'disetujui' || s === 'diterima') return 'text-blue-800 bg-blue-100'; if (s === 'ditolak') return 'text-red-800 bg-red-100'; return 'text-orange-800 bg-orange-100'; }; const formatCurrency = (amount?: number) => new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(amount || 0); return (

{formatCurrency(item.Total_Tagihan)} | LUAS = {item.Luas || 'N/A'} M2

{item.Nama_Pengguna} : {item.Jenis_Kegiatan}

{item.Tempat}

{(item.Surat_Permohonan_Link || item.Bukti_Bayar_Link) && (
{item.Surat_Permohonan_Link && ( Surat Permohonan )} {item.Bukti_Bayar_Link && ( Bukti Bayar )}
)}

{user?.role === 'admin' ? item.Email_Penyewa : ''}

{item.Status_Pembayaran || 'Diproses'}
); }; const SewaLahanScreen = () => { const [activeFilter, setActiveFilter] = useState('All'); const [searchTerm, setSearchTerm] = useState(''); const filterOptions = useMemo(() => { const statuses = [...new Set(sewaData.map(item => item.Status_Pembayaran).filter(Boolean) as string[])]; return ['All', ...statuses]; }, [sewaData]); const filteredData = useMemo(() => { const lowercasedFilter = searchTerm.toLowerCase(); const data = sewaData.filter(item => { const matchesFilter = activeFilter === 'All' || item.Status_Pembayaran === activeFilter; if (!matchesFilter) return false; if (searchTerm === '') return true; return ( item.Nama_Pengguna?.toLowerCase().includes(lowercasedFilter) || item.Jenis_Kegiatan?.toLowerCase().includes(lowercasedFilter) || item.Tempat?.toLowerCase().includes(lowercasedFilter) ); }); return data.sort((a, b) => { const idA = parseInt(a.ID, 10) || 0; const idB = parseInt(b.ID, 10) || 0; return idB - idA; }); }, [sewaData, activeFilter, searchTerm]); return (

Sewa Lahan

setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-full bg-white shadow-sm" />
{filterOptions.map(status => ( ))}
{isLoading ? (
) : filteredData.length > 0 ? (
{filteredData.map(item => { setEditingItem(item); setIsModalOpen(true); }} />)}
) : (

Tidak ada data yang cocok untuk "{searchTerm || activeFilter}".

)}
); }; const SewaFormModal = ({ onSubmit, onClose, itemToEdit }: { onSubmit: (e: FormEvent) => void; onClose: () => void; itemToEdit: SewaData | null; }) => { const isEditMode = !!itemToEdit; const isPaymentApproved = itemToEdit?.Status_Pembayaran?.toLowerCase() === 'disetujui' || itemToEdit?.Status_Pembayaran?.toLowerCase() === 'diterima'; // State untuk checkbox biaya tambahan const [isTendaChecked, setIsTendaChecked] = useState(false); const [isListrikChecked, setIsListrikChecked] = useState(false); useEffect(() => { if (itemToEdit) { if (itemToEdit.Biaya_Tenda && Number(itemToEdit.Biaya_Tenda) > 0) setIsTendaChecked(true); if (itemToEdit.Biaya_Listrik && Number(itemToEdit.Biaya_Listrik) > 0) setIsListrikChecked(true); } }, [itemToEdit]); const formatDateForInput = (dateString?: string) => { if (!dateString) return ''; try { return new Date(dateString).toISOString().split('T')[0]; } catch { return ''; } }; return (
e.stopPropagation()}>

{isEditMode ? 'Edit Sewa' : 'Sewa Lahan'}

{/* Bagian Biaya Tambahan */}
{/* Biaya Tenda */}
setIsTendaChecked(e.target.checked)} className="w-5 h-5 text-red-600 rounded border-gray-300 focus:ring-red-500" />
{isTendaChecked && (
)}
{/* Biaya Listrik */}
setIsListrikChecked(e.target.checked)} className="w-5 h-5 text-red-600 rounded border-gray-300 focus:ring-red-500" />
{isListrikChecked && (
)}

{isEditMode && !isPaymentApproved && (

Upload bukti bayar hanya bisa dilakukan jika status "Disetujui".

)}
); }; const BiayaScreen = () => { const accountNumber = '9881954325712301'; const copyToClipboard = () => { navigator.clipboard.writeText(accountNumber); setToastMessage('Nomor rekening berhasil disalin!'); }; return (
{/* Bagian BIAYA */}

BIAYA

Biaya dihitung berdasarkan Peraturan Rektor No. 80 Tahun 2024 tentang Tarif. Untuk sewa bazar berukuran 2 x 2 meter dikenakan biaya sebesar Rp 224.000,-.

{/* Bagian CARA PEMBAYARAN */}

CARA PEMBAYARAN

  • Pembayaran sewa sebagaimana dimaksud pada Pasal 3 ayat (1) dilakukan oleh PIHAK KEDUA kepada PIHAK PERTAMA dengan cara disetorkan ke rekening atas nama IGU Dit. Bisnis Sewa Lahan dan Booth dengan nomor rekening {accountNumber} pada Bank BNI.
  • Setelah pembayaran dilakukan, PIHAK KEDUA wajib menyerahkan bukti transfer kepada PIHAK PERTAMA. Selanjutnya akan diterbitkan invoice dan/atau tanda terima yang sah atas penerimaan sejumlah uang tersebut.
{/* Tombol Salin Nomor Rekening */}
); }; const Toast = () => { if (!toastMessage) return null; return
{toastMessage}
; }; if (isLoading && !user) { return
; } return ( <> {user ? : } ); }; // Daftarkan Service Worker if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('sw.js') .then(registration => { console.log('ServiceWorker registration successful with scope: ', registration.scope); }) .catch(error => { console.log('ServiceWorker registration failed: ', error); }); }); } // Render aplikasi const container = document.getElementById('root'); if (container) { const root = ReactDOM.createRoot(container); root.render( ); }