import React, { useState, useEffect, useRef } from 'react'; import { Printer, Save, List, Plus, Receipt, User, Calendar, CreditCard, X, MessageCircle, Share2, Download, ImageIcon, Lock, LogOut, Users, Edit2, Trash2, Shield, Database, Copy, Check } from 'lucide-react'; // ============================================================================ // KONFIGURASI SISTEM (UNTUK HOSTING & MYSQL) // ============================================================================ // Ubah MOCK_MODE menjadi false jika API backend PHP sudah di-hosting const CONFIG = { MOCK_MODE: false, API_BASE_URL: 'https://qurban.brayat39.com/api.php' // Ganti dengan URL file PHP Anda di server }; // --- SIMULASI DATABASE UNTUK PREVIEW CANVAS (DENGAN CONTOH DATA) --- const initMockDB = () => { if (!localStorage.getItem('db_users')) { localStorage.setItem('db_users', JSON.stringify([ { id: '1', username: 'admin', password: 'password123', name: 'Administrator Pusat', role: 'admin' }, { id: '2', username: 'panitia1', password: 'sandi123', name: 'Budi Santoso', role: 'panitia' }, { id: '3', username: 'panitia2', password: 'sandi123', name: 'Siti Aminah', role: 'panitia' } ])); } if (!localStorage.getItem('db_kwitansi') || JSON.parse(localStorage.getItem('db_kwitansi')).length === 0) { const year = new Date().getFullYear(); localStorage.setItem('db_kwitansi', JSON.stringify([ { id: '102', noKwitansi: `QBN-${year}-002`, namaShohibul: 'H. Abdullah bin Umar', alamat: 'Jl. Merdeka No. 45, Jakarta', jenisHewan: 'Sapi Utuh (1/1)', jumlahDana: 24500000, terbilang: 'Dua Puluh Empat Juta Lima Ratus Ribu Rupiah', tanggal: new Date().toISOString().split('T')[0], keterangan: 'Lunas, Sapi Limosin', namaPanitia: 'Siti Aminah' }, { id: '101', noKwitansi: `QBN-${year}-001`, namaShohibul: 'Keluarga Bpk. Haryanto', alamat: 'Perum. Indah Blok B/12', jenisHewan: 'Sapi (1/7)', jumlahDana: 3500000, terbilang: 'Tiga Juta Lima Ratus Ribu Rupiah', tanggal: new Date(Date.now() - 86400000).toISOString().split('T')[0], keterangan: 'Kolektif Sapi ke-1', namaPanitia: 'Budi Santoso' } ])); } }; if (CONFIG.MOCK_MODE) initMockDB(); // --- API SERVICE --- const api = { login: async (username, password) => { if (CONFIG.MOCK_MODE) { return new Promise((resolve) => setTimeout(() => { const users = JSON.parse(localStorage.getItem('db_users')); const user = users.find(u => u.username === username && u.password === password); if (user) resolve({ success: true, user: { id: user.id, username: user.username, name: user.name, role: user.role } }); else resolve({ success: false, message: 'Username atau password salah' }); }, 500)); } else { try { const res = await fetch(`${CONFIG.API_BASE_URL}?action=login`, { method: 'POST', body: JSON.stringify({ username, password }) }); return res.json(); } catch (err) { return { success: false, message: 'Gagal terhubung ke server PHP' }; } } }, getUsers: async () => { if (CONFIG.MOCK_MODE) { return new Promise(resolve => setTimeout(() => { const users = JSON.parse(localStorage.getItem('db_users')); resolve({ success: true, data: users.filter(u => u.role !== 'admin') }); }, 300)); } else { const res = await fetch(`${CONFIG.API_BASE_URL}?action=get_users`); return res.json(); } }, saveUser: async (userData) => { if (CONFIG.MOCK_MODE) { return new Promise(resolve => setTimeout(() => { let users = JSON.parse(localStorage.getItem('db_users')); if (userData.id) { users = users.map(u => u.id === userData.id ? { ...u, ...userData, password: userData.password || u.password } : u); } else { const exists = users.find(u => u.username === userData.username); if (exists) return resolve({ success: false, message: 'Username sudah digunakan' }); users.push({ ...userData, id: Date.now().toString(), role: 'panitia' }); } localStorage.setItem('db_users', JSON.stringify(users)); resolve({ success: true, message: 'Data panitia berhasil disimpan' }); }, 300)); } else { const res = await fetch(`${CONFIG.API_BASE_URL}?action=save_user`, { method: 'POST', body: JSON.stringify(userData) }); return res.json(); } }, deleteUser: async (id) => { if (CONFIG.MOCK_MODE) { return new Promise(resolve => setTimeout(() => { let users = JSON.parse(localStorage.getItem('db_users')); users = users.filter(u => u.id !== id); localStorage.setItem('db_users', JSON.stringify(users)); resolve({ success: true, message: 'Panitia berhasil dihapus' }); }, 300)); } else { const res = await fetch(`${CONFIG.API_BASE_URL}?action=delete_user`, { method: 'POST', body: JSON.stringify({ id }) }); return res.json(); } }, getKwitansi: async () => { if (CONFIG.MOCK_MODE) { return new Promise(resolve => setTimeout(() => resolve({ success: true, data: JSON.parse(localStorage.getItem('db_kwitansi')) }), 300)); } else { const res = await fetch(`${CONFIG.API_BASE_URL}?action=get_kwitansi`); return res.json(); } }, saveKwitansi: async (data) => { if (CONFIG.MOCK_MODE) { return new Promise(resolve => setTimeout(() => { let kwitansi = JSON.parse(localStorage.getItem('db_kwitansi')); kwitansi.unshift({ ...data, id: Date.now().toString() }); localStorage.setItem('db_kwitansi', JSON.stringify(kwitansi)); resolve({ success: true, data: kwitansi }); }, 300)); } else { const res = await fetch(`${CONFIG.API_BASE_URL}?action=save_kwitansi`, { method: 'POST', body: JSON.stringify(data) }); return res.json(); } } }; // ============================================================================ function terbilang(angka) { const bilangan = ['', 'Satu', 'Dua', 'Tiga', 'Empat', 'Lima', 'Enam', 'Tujuh', 'Delapan', 'Sembilan', 'Sepuluh', 'Sebelas']; let temp = ''; if (angka < 12) temp = ' ' + bilangan[angka]; else if (angka < 20) temp = terbilang(angka - 10) + ' Belas'; else if (angka < 100) temp = terbilang(Math.floor(angka / 10)) + ' Puluh' + terbilang(angka % 10); else if (angka < 200) temp = ' Seratus' + terbilang(angka - 100); else if (angka < 1000) temp = terbilang(Math.floor(angka / 100)) + ' Ratus' + terbilang(angka % 100); else if (angka < 2000) temp = ' Seribu' + terbilang(angka - 1000); else if (angka < 1000000) temp = terbilang(Math.floor(angka / 1000)) + ' Ribu' + terbilang(angka % 1000); else if (angka < 1000000000) temp = terbilang(Math.floor(angka / 1000000)) + ' Juta' + terbilang(angka % 1000000); else if (angka < 1000000000000) temp = terbilang(Math.floor(angka / 1000000000)) + ' Miliar' + terbilang(angka % 1000000000); return temp.trim(); } const formatRupiah = (number) => new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(number); const ReceiptCard = ({ data }) => { if (!data) return null; return (

Kwitansi Qurban

Panitia Penerimaan & Penyaluran Hewan Qurban

No. Kwitansi
{data.noKwitansi}
Telah terima dari
:
{data.namaShohibul} {data.alamat ? `— ${data.alamat}` : ''}
Uang Sejumlah
:
{data.terbilang}
Untuk Pembayaran
:
Dana Ibadah Qurban berupa {data.jenisHewan}. {data.keterangan && Catatan: {data.keterangan}}
Terbilang {formatRupiah(data.jumlahDana)}

{new Date(data.tanggal).toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
Panitia Qurban / Penerima,

{data.namaPanitia || '( ......................................... )'}
"Semoga Allah SWT menerima ibadah qurban kita. Aamiin."
); }; // ============================================================================ // KOMPONEN DASHBOARD PANITIA (CRUD Kwitansi) // ============================================================================ const PanitiaDashboard = ({ currentUser }) => { const [entries, setEntries] = useState([]); const [currentPrint, setCurrentPrint] = useState(null); const [showPreview, setShowPreview] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [isLoading, setIsLoading] = useState(true); const [formData, setFormData] = useState({ noKwitansi: '', namaShohibul: '', alamat: '', jenisHewan: 'Sapi (1/7)', jumlahDana: '', tanggal: new Date().toISOString().split('T')[0], keterangan: '' }); useEffect(() => { loadKwitansi(); }, []); useEffect(() => { generateNoKwitansi(); }, [entries]); const loadKwitansi = async () => { setIsLoading(true); const res = await api.getKwitansi(); if (res.success) setEntries(res.data); setIsLoading(false); }; const generateNoKwitansi = () => { const year = new Date().getFullYear(); const count = entries.length + 1; const paddedCount = String(count).padStart(3, '0'); setFormData(prev => ({ ...prev, noKwitansi: `QBN-${year}-${paddedCount}` })); }; const handleInputChange = (e) => setFormData(prev => ({ ...prev, [e.target.name]: e.target.value })); const handleSubmit = async (e, isPrintAndSave = false) => { e.preventDefault(); if (!formData.namaShohibul || !formData.jumlahDana) return; const newEntry = { ...formData, namaPanitia: currentUser.name, jumlahDana: parseFloat(formData.jumlahDana), terbilang: terbilang(parseFloat(formData.jumlahDana)) + ' Rupiah' }; setIsProcessing(true); const res = await api.saveKwitansi(newEntry); if (res.success) { if(CONFIG.MOCK_MODE) { setEntries(res.data); setCurrentPrint(res.data[0]); } else { await loadKwitansi(); setCurrentPrint(newEntry); } setFormData({ noKwitansi: '', namaShohibul: '', alamat: '', jenisHewan: 'Sapi (1/7)', jumlahDana: '', tanggal: formData.tanggal, keterangan: '' }); if (isPrintAndSave) setShowPreview(true); } else alert('Gagal menyimpan kwitansi.'); setIsProcessing(false); }; const handlePrint = (entry) => { setCurrentPrint(entry); setShowPreview(true); }; const triggerPrintSubmit = (e) => { handleSubmit(e, true); }; const generateImageBlob = async () => { if (!window.html2canvas) return alert('Sistem sedang memuat modul gambar. Coba sebentar lagi.'); const element = document.getElementById('receipt-print-area'); if (!element) return null; try { const canvas = await window.html2canvas(element, { scale: 2, backgroundColor: '#ffffff', useCORS: true }); return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), 'image/png')); } catch (err) { return null; } }; const handleDownloadImage = async () => { setIsProcessing(true); const blob = await generateImageBlob(); if (blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Kwitansi_Qurban_${currentPrint.noKwitansi}.png`; a.click(); URL.revokeObjectURL(url); } setIsProcessing(false); }; const handleShareImage = async () => { setIsProcessing(true); const blob = await generateImageBlob(); if (blob) { const file = new File([blob], `Kwitansi_Qurban_${currentPrint.noKwitansi}.png`, { type: 'image/png' }); if (navigator.canShare && navigator.canShare({ files: [file] })) { try { await navigator.share({ title: `Kwitansi Qurban - ${currentPrint.namaShohibul}`, text: 'Berikut bukti kwitansi qurban.', files: [file] }); } catch (err) { } } else { alert('Fitur bagikan gambar tidak didukung di browser ini. Gambar akan diunduh.'); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = file.name; a.click(); URL.revokeObjectURL(url); } } setIsProcessing(false); }; return ( <>

Input Data Baru

handleSubmit(e, false)} className="space-y-4">
Rp

Daftar Shohibul Terdaftar

{entries.length} Data
{isLoading ? (
Memuat data...
) : entries.length === 0 ? (

Belum ada data shohibul yang didaftarkan.

) : (
{entries.map((entry) => ( ))}
No. Kwitansi Nama Shohibul Nominal Penerima Aksi
{entry.noKwitansi}
{entry.namaShohibul}
{entry.tanggal}
{formatRupiah(entry.jumlahDana)} {entry.namaPanitia}
)}
{showPreview && currentPrint && (

Preview Kwitansi

)}
); }; // ============================================================================ // KOMPONEN GENERATOR FILE AUTO-INSTALLER (PHP BACKEND) // ============================================================================ const ServerSetup = () => { const [copied, setCopied] = useState(false); const phpInstallerCode = ``; const copyToClipboard = () => { navigator.clipboard.writeText(phpInstallerCode); setCopied(true); setTimeout(() => setCopied(false), 3000); }; const downloadPHP = () => { const blob = new Blob([phpInstallerCode], { type: 'application/x-httpd-php' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'api.php'; a.click(); URL.revokeObjectURL(url); }; return (

Integrasi & Auto-Install Server MySQL

Cara Mengaktifkan Database Asli (CPanel / Localhost):

  1. Klik tombol Unduh File api.php di bawah ini.
  2. Upload file `api.php` tersebut ke hosting Anda (misal: di dalam folder public_html/qurban/) atau folder XAMPP htdocs/.
  3. Buka file `app.jsx` (Aplikasi React ini), lalu ubah baris MOCK_MODE: false.
  4. Ganti API_BASE_URL dengan link hosting Anda (contoh: https://qurban.brayat39.com/api.php).
  5. Selesai! Saat API pertama kali diakses, sistem akan Otomatis Menginstall Database, Tabel, dan Sample Data Shohibul & Panitia.
          {phpInstallerCode}
        
); }; // ============================================================================ // KOMPONEN DASHBOARD ADMIN (CRUD Panitia & Server) // ============================================================================ const AdminDashboard = () => { const [users, setUsers] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [activeTab, setActiveTab] = useState('panitia'); // 'panitia' atau 'server' const [formData, setFormData] = useState({ id: '', username: '', name: '', password: '' }); useEffect(() => { loadUsers(); }, []); const loadUsers = async () => { const res = await api.getUsers(); if (res.success) setUsers(res.data); }; const handleOpenModal = (user = null) => { if (user) setFormData({ id: user.id, username: user.username, name: user.name, password: '' }); else setFormData({ id: '', username: '', name: '', password: '' }); setIsModalOpen(true); }; const handleSubmit = async (e) => { e.preventDefault(); if (!formData.username || !formData.name || (!formData.id && !formData.password)) return alert('Mohon lengkapi data wajib.'); setIsProcessing(true); const res = await api.saveUser(formData); if (res.success) { setIsModalOpen(false); loadUsers(); } else alert(res.message); setIsProcessing(false); }; const handleDelete = async (id) => { if (window.confirm('Yakin ingin menghapus panitia ini?')) { const res = await api.deleteUser(id); if (res.success) loadUsers(); } }; return (
{/* Tab Navigasi Admin */}
{activeTab === 'panitia' ? (

Kelola Data Panitia

{users.length === 0 && ( )} {users.map((u) => ( ))}
Nama Lengkap Username (Login) Aksi
Belum ada data panitia terdaftar.
{u.name} {u.username}
) : ( )} {/* Modal Form CRUD Panitia */} {isModalOpen && (

{formData.id ? 'Edit Panitia' : 'Tambah Panitia Baru'}

setFormData({...formData, name: e.target.value})} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-emerald-500 focus:border-emerald-500" placeholder="Cth: Ahmad Fulan" />
setFormData({...formData, username: e.target.value})} disabled={!!formData.id} className={`w-full px-3 py-2 border border-gray-300 rounded-lg ${formData.id ? 'bg-gray-100' : 'focus:ring-emerald-500 focus:border-emerald-500'}`} placeholder="Cth: ahmad123" />
setFormData({...formData, password: e.target.value})} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-emerald-500 focus:border-emerald-500" placeholder="Minimal 6 karakter" />
)}
); }; // ============================================================================ // KOMPONEN UTAMA (ROUTER & LOGIN) // ============================================================================ export default function App() { const [currentUser, setCurrentUser] = useState(null); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [isLoggingIn, setIsLoggingIn] = useState(false); useEffect(() => { const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js'; script.async = true; document.body.appendChild(script); }, []); const handleLogin = async (e) => { e.preventDefault(); setIsLoggingIn(true); const res = await api.login(username, password); if (res.success) { setCurrentUser(res.user); } else { alert(res.message || 'Login gagal!'); } setIsLoggingIn(false); }; const handleLogout = () => { setCurrentUser(null); setUsername(''); setPassword(''); }; if (!currentUser) { return (

Masuk Sistem

Aplikasi Kwitansi Qurban Digital

Akun Login Tersedia: • Admin : admin / sandi: password123
• Panitia : panitia1 / sandi: sandi123
setUsername(e.target.value)} placeholder="Masukkan username" className="w-full pl-10 px-4 py-3 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500" />
setPassword(e.target.value)} placeholder="Masukkan kata sandi" className="w-full pl-10 px-4 py-3 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500" />
); } return (

Sistem Kwitansi Qurban

Mode: {currentUser.role === 'admin' ? 'Administrator' : 'Panitia Penerima'}

Login sebagai:

{currentUser.name}

{currentUser.role === 'admin' ? : }
); }