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}` : ''}
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 (
<>
Daftar Shohibul Terdaftar
{entries.length} Data
{isLoading ? (
Memuat data...
) : entries.length === 0 ? (
Belum ada data shohibul yang didaftarkan.
) : (
| No. Kwitansi |
Nama Shohibul |
Nominal |
Penerima |
Aksi |
{entries.map((entry) => (
| {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):
- Klik tombol Unduh File api.php di bawah ini.
- Upload file `api.php` tersebut ke hosting Anda (misal: di dalam folder
public_html/qurban/) atau folder XAMPP htdocs/.
- Buka file `app.jsx` (Aplikasi React ini), lalu ubah baris
MOCK_MODE: false.
- Ganti
API_BASE_URL dengan link hosting Anda (contoh: https://qurban.brayat39.com/api.php).
- Selesai! Saat API pertama kali diakses, sistem akan Otomatis Menginstall Database, Tabel, dan Sample Data Shohibul & Panitia.
);
};
// ============================================================================
// 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
| Nama Lengkap |
Username (Login) |
Aksi |
{users.length === 0 && (
| Belum ada data panitia terdaftar. |
)}
{users.map((u) => (
| {u.name} |
{u.username} |
|
))}
) : (
)}
{/* Modal Form CRUD Panitia */}
{isModalOpen && (
{formData.id ? 'Edit Panitia' : 'Tambah Panitia Baru'}
)}
);
};
// ============================================================================
// 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
);
}
return (
Sistem Kwitansi Qurban
Mode: {currentUser.role === 'admin' ? 'Administrator' : 'Panitia Penerima'}
Login sebagai:
{currentUser.name}
{currentUser.role === 'admin' ? : }
);
}