/* ════════════════════════════════════════════════
Dashboard — modul Konten generik + Donasi + Pengaturan
════════════════════════════════════════════════ */
/* Skema field per jenis konten */
const PAGE_OPTS = [
{ value: 'articles', label: 'Halaman Artikel' },
{ value: 'donasi', label: 'Halaman Donasi' },
{ value: 'register', label: 'Pendaftaran Santri' },
{ value: 'programs', label: 'Halaman Program' },
{ value: 'kegiatan', label: 'Halaman Kegiatan' },
{ value: 'toko', label: 'Toko Buku' },
{ value: 'about', label: 'Tentang Kami' },
{ value: 'contact', label: 'Kontak' },
];
const ACCENT_OPTS = [
{ value: '#1F6B47', label: 'Hijau (Sage)' },
{ value: '#1B2F6E', label: 'Biru (Navy)' },
{ value: '#C26F12', label: 'Oranye' },
{ value: '#8B5A2B', label: 'Coklat' },
];
const KIND_GLYPH = { banner: 'scroll', program: 'cap', teacher: 'mosque', testimonial: 'scroll', fasilitas: 'mosque', kegiatan: 'calendar', buku: 'book' };
const KIND_SCHEMA = {
banner: {
title: 'Banner Beranda', singular: 'Banner', icon: '🖼️',
fields: [
{ key: 'title', label: 'Judul Banner', type: 'text', primary: true },
{ key: 'subtitle', label: 'Subjudul / Deskripsi', type: 'textarea' },
{ key: 'tag', label: 'Label kecil (mis. Artikel Baru)', type: 'text' },
{ key: 'image_url', label: 'Gambar Latar', type: 'image' },
{ key: 'cta_label', label: 'Teks Tombol (mis. Baca Selengkapnya)', type: 'text' },
{ key: 'cta_target', label: 'Tombol Menuju Halaman', type: 'select', options: PAGE_OPTS },
{ key: 'accent', label: 'Warna Aksen', type: 'select', options: ACCENT_OPTS },
],
},
program: {
title: 'Program', singular: 'Program', icon: '🎓',
fields: [
{ key: 'name', label: 'Nama Program', type: 'text', primary: true },
{ key: 'icon', label: 'Ikon (emoji)', type: 'text' },
{ key: 'desc', label: 'Deskripsi', type: 'textarea' },
{ key: 'schedule', label: 'Jadwal', type: 'text' },
{ key: 'levels', label: 'Level (pisahkan koma)', type: 'list' },
{ key: 'banner_url', label: 'Gambar Banner (opsional)', type: 'image' },
{ key: 'youtube_url', label: 'Link Video YouTube (opsional)', type: 'text' },
{ key: 'featured', label: 'Tampilkan di Beranda?', type: 'toggle' },
{ key: 'banner_home', label: 'Jadikan Banner Slideshow Beranda?', type: 'toggle' },
],
},
teacher: {
title: 'Pengajar', singular: 'Pengajar', icon: '👤',
fields: [
{ key: 'name', label: 'Nama', type: 'text', primary: true },
{ key: 'title', label: 'Jabatan / Keahlian', type: 'text' },
{ key: 'bio', label: 'Bio singkat', type: 'textarea' },
{ key: 'photo_url', label: 'Foto', type: 'image' },
{ key: 'tags', label: 'Keahlian (pisahkan koma)', type: 'list' },
],
},
testimonial: {
title: 'Testimoni', singular: 'Testimoni', icon: '⭐',
fields: [
{ key: 'name', label: 'Nama', type: 'text', primary: true },
{ key: 'role', label: 'Peran (mis. Wali Santri)', type: 'text' },
{ key: 'text', label: 'Isi Testimoni', type: 'textarea' },
{ key: 'stars', label: 'Bintang (1-5)', type: 'number' },
],
},
fasilitas: {
title: 'Fasilitas', singular: 'Fasilitas', icon: '🏫',
fields: [
{ key: 'caption', label: 'Nama Fasilitas', type: 'text', primary: true },
{ key: 'img_url', label: 'Foto', type: 'image' },
{ key: 'emoji', label: 'Ikon (emoji)', type: 'text' },
],
},
kegiatan: {
title: 'Kegiatan', singular: 'Kegiatan', icon: '📅',
fields: [
{ key: 'title', label: 'Judul Kegiatan', type: 'text', primary: true },
{ key: 'date', label: 'Tanggal', type: 'text' },
{ key: 'time', label: 'Waktu (opsional)', type: 'text' },
{ key: 'location', label: 'Lokasi (opsional)', type: 'text' },
{ key: 'desc', label: 'Deskripsi', type: 'textarea' },
{ key: 'img_url', label: 'Foto', type: 'image' },
{ key: 'youtube_url', label: 'Link Video YouTube (opsional)', type: 'text' },
{ key: 'featured', label: 'Tampilkan di Beranda?', type: 'toggle' },
{ key: 'banner_home', label: 'Jadikan Banner Slideshow Beranda?', type: 'toggle' },
],
},
buku: {
title: 'Toko Buku', singular: 'Buku', icon: '🛒',
fields: [
{ key: 'title', label: 'Judul Buku', type: 'text', primary: true },
{ key: 'author', label: 'Penulis', type: 'text' },
{ key: 'price', label: 'Harga (Rp)', type: 'number' },
{ key: 'originalPrice', label: 'Harga Coret (opsional)', type: 'number' },
{ key: 'desc', label: 'Deskripsi', type: 'textarea' },
{ key: 'cover_url', label: 'Sampul', type: 'image' },
{ key: 'emoji', label: 'Ikon (emoji)', type: 'text' },
{ key: 'stock', label: 'Stok', type: 'number' },
{ key: 'categories', label: 'Kategori (pisahkan koma)', type: 'list' },
{ key: 'featured', label: 'Unggulan (badge)?', type: 'toggle' },
{ key: 'banner_home', label: 'Jadikan Banner Slideshow Beranda?', type: 'toggle' },
],
},
};
function ContentListModule({ section, kind, toast }) {
const schema = KIND_SCHEMA[kind];
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [modal, setModal] = useState(null);
const [form, setForm] = useState({});
const [saving, setSaving] = useState(false);
const load = () => {
setLoading(true);
// mula-mula dari data lokal sebagai fallback
const seed = (window.MQA || {})[section] || [];
api.get(`content.php?action=get_section§ion=${section}`).then(r => {
const arr = (r.ok && r.data && Array.isArray(r.data.items)) ? r.data.items : seed;
setItems(arr);
}).catch(() => setItems(seed)).finally(() => setLoading(false));
};
useEffect(load, [section]);
const persist = async (next) => {
setItems(next);
const r = await api.post('content.php?action=save', { section, data: { items: next } });
if (r.ok) toast('Tersimpan'); else toast('Gagal menyimpan');
};
const primaryKey = schema.fields.find(f => f.primary).key;
const blank = () => { const o = {}; schema.fields.forEach(f => o[f.key] = f.type === 'list' ? [] : f.type === 'toggle' ? false : ''); return o; };
const openAdd = () => { setForm(blank()); setModal({}); };
const openEdit = (it, i) => { setForm({ ...it }); setModal({ i }); };
const save = async () => {
if (!form[primaryKey]) return;
setSaving(true);
// normalisasi number/list
const clean = { ...form };
schema.fields.forEach(f => {
if (f.type === 'number') clean[f.key] = clean[f.key] === '' || clean[f.key] == null ? null : Number(clean[f.key]);
});
let next;
if (modal.i != null) next = items.map((x, i) => i === modal.i ? { ...x, ...clean } : x);
else next = [{ id: Date.now(), ...clean }, ...items];
await persist(next);
setSaving(false); setModal(null);
};
const del = async (i) => {
if (!window.confirm('Hapus item ini?')) return;
await persist(items.filter((_, j) => j !== i));
};
return (
Kelola {schema.title.toLowerCase()} yang tampil di situs.
+ Tambah {schema.singular}
{loading ?
Memuat…
: items.length === 0 ?
Belum ada {schema.title.toLowerCase()}.
:
{items.map((it, i) => {
const img = it.cover_url || it.img_url || it.photo_url;
return (
{img ?

:
}
{it[primaryKey]}
{it.desc || it.text || it.title || it.role || it.author || it.schedule || ''}
openEdit(it, i)} style={{ flex: 1 }}>Edit
del(i)}>Hapus
);
})}
}
setModal(null)} title={modal && modal.i != null ? `Edit ${schema.singular}` : `Tambah ${schema.singular}`} width="560px">
{modal && schema.fields.map(f => {
const val = form[f.key];
if (f.type === 'image') return setForm(s => ({ ...s, [f.key]: v }))} ratio={kind === 'buku' || kind === 'teacher' ? '3/4' : '16/9'} />;
if (f.type === 'textarea') return ;
if (f.type === 'list') return setForm(s => ({ ...s, [f.key]: e.target.value.split(',').map(x => x.trim()).filter(Boolean) }))} style={dInp} />;
if (f.type === 'number') return setForm(s => ({ ...s, [f.key]: e.target.value }))} style={dInp} />;
if (f.type === 'select') return ;
if (f.type === 'toggle') return ;
return setForm(s => ({ ...s, [f.key]: e.target.value }))} style={dInp} />;
})}
{saving ? 'Menyimpan…' : 'Simpan'}
setModal(null)}>Batal
);
}
/* ── Donasi: kelola campaign ── */
function DonasiModule({ toast }) {
const [camps, setCamps] = useState([]);
const [loading, setLoading] = useState(true);
const [modal, setModal] = useState(null);
const [form, setForm] = useState({});
const [saving, setSaving] = useState(false);
const load = () => { setLoading(true); api.get('donations.php?action=list_campaigns').then(r => { if (r.ok && r.campaigns) setCamps(r.campaigns); }).finally(() => setLoading(false)); };
useEffect(load, []);
const openAdd = () => { setForm({ title: '', description: '', target: 10000000, collected: 0, urgent: 0, status: 'aktif', image_url: '' }); setModal({}); };
const openEdit = (c) => { setForm({ ...c }); setModal({ id: c.id }); };
const save = async () => {
if (!form.title) return;
setSaving(true);
const action = modal.id ? 'update_campaign' : 'create_campaign';
const r = await api.post('donations.php?action=' + action, modal.id ? { ...form, id: modal.id } : form);
// mock belum punya endpoint ini → fallback: simpan via list lokal
if (r.ok) { toast('Tersimpan'); setModal(null); load(); }
else {
// fallback lokal supaya tetap terasa di preview
setCamps(p => modal.id ? p.map(c => c.id === modal.id ? { ...c, ...form } : c) : [{ ...form, id: Date.now(), donor_count: 0 }, ...p]);
toast('Tersimpan'); setModal(null);
}
setSaving(false);
};
return (
Kelola program donasi & pantau perolehan.
+ Program Donasi
{loading ?
Memuat…
:
{camps.map(c => {
const pct = c.target ? Math.min(100, Math.round((c.collected / c.target) * 100)) : 0;
return (
{c.title}
{c.urgent ?
Mendesak :
{c.status === 'selesai' ? 'Selesai' : 'Aktif'}}
{fmtRp(c.collected)}
{pct}% dari {fmtRp(c.target)}
openEdit(c)} style={{ width: '100%' }}>Edit Program
);
})}
}
setModal(null)} title={modal && modal.id ? 'Edit Program Donasi' : 'Program Donasi Baru'}>
{modal && (
<>
setForm(f => ({ ...f, image_url: v }))} />
setForm(f => ({ ...f, title: e.target.value }))} style={dInp} />
setForm(f => ({ ...f, target: Number(e.target.value) }))} style={dInp} />
setForm(f => ({ ...f, collected: Number(e.target.value) }))} style={dInp} />
{saving ? 'Menyimpan…' : 'Simpan'}
setModal(null)}>Batal
>
)}
);
}
/* ── Kartu pengaturan donasi: QRIS, WA konfirmasi, rekening ── */
function DonasiSettingsCard({ toast }) {
const [s, setS] = useState({ qris_url: '', donasi_wa_konfirmasi: '' });
const [rek, setRek] = useState([]);
const [saving, setSaving] = useState(false);
useEffect(() => {
api.get('donations.php?action=get_settings').then(r => { if (r.ok && r.settings) setS(v => ({ ...v, ...r.settings })); });
api.get('donations.php?action=list_rekening').then(r => { if (r.ok && r.rekening) setRek(r.rekening); });
}, []);
const save = async () => { setSaving(true); const r = await api.post('donations.php?action=save_settings', s); if (r.ok) toast('Pengaturan donasi disimpan'); setSaving(false); };
return (
Metode Pembayaran Donasi
setS(f => ({ ...f, qris_url: v }))} ratio="1/1" />
setS(f => ({ ...f, donasi_wa_konfirmasi: e.target.value }))} style={dInp} placeholder="628xxxxxxxxxx" />
{saving ? 'Menyimpan…' : 'Simpan Metode Pembayaran'}
Rekening Terdaftar ({rek.length})
{rek.length === 0
?
Belum ada rekening. Tambahkan lewat panel rekening (kelola-donasi) di server.
: rek.map((r, i) =>
• {r.bank} {r.nomor || r.number} — a.n. {r.atas_nama || r.name}
)}
Pilihan "Metode" di form donasi otomatis mengikuti rekening ini + QRIS (bila gambar diisi).
);
}
/* ── Pengaturan situs: info, profil (Tentang), kontak ── */
function SettingsModule({ toast }) {
const [form, setForm] = useState({
site_name: "Markaz Qurrota A'yun", tagline: '', tahun: '2015',
profil: '', profil2: '', stat_santri: '500+', stat_pengajar: '25+', stat_program: '8',
phone: '0812-3456-7890', wa: '628123456789', email: 'info@qurrotaayun.com',
address: '', jam: 'Senin–Sabtu, 08.00–20.00 WIB',
ig: '', yt: '', fb: '', tiktok: '', maps_embed: '',
});
const [saving, setSaving] = useState(false);
useEffect(() => { api.get('content.php?action=get_section§ion=settings').then(r => { if (r.ok && r.data) setForm(f => ({ ...f, ...r.data })); }); }, []);
const save = async () => { setSaving(true); const r = await api.post('content.php?action=save', { section: 'settings', data: form }); if (r.ok) toast('Pengaturan disimpan'); setSaving(false); };
const F = (k) => (e) => setForm(f => ({ ...f, [k]: e.target.value }));
return (
);
}
/* ── Header Halaman: edit banner tiap halaman (teks Arab, judul, deskripsi, foto) ── */
const PAGE_LIST = [
['articles', 'Artikel'], ['programs', 'Program'], ['teachers', 'Pengajar'],
['gallery', 'Fasilitas'], ['kegiatan', 'Kegiatan'], ['toko', 'Toko Buku'],
['donasi', 'Donasi'], ['register', 'Pendaftaran'], ['about', 'Tentang'], ['contact', 'Kontak'],
];
function PageHeadersModule({ toast }) {
const [data, setData] = useState({});
const [active, setActive] = useState('articles');
const [saving, setSaving] = useState(false);
useEffect(() => { api.get('content.php?action=get_section§ion=page_headers').then(r => { if (r.ok && r.data) setData(r.data); }); }, []);
const cur = data[active] || {};
const upd = (k, v) => setData(d => ({ ...d, [active]: { ...d[active], [k]: v } }));
const save = async () => { setSaving(true); const r = await api.post('content.php?action=save', { section: 'page_headers', data }); if (r.ok) toast('Header halaman disimpan'); setSaving(false); };
return (
{PAGE_LIST.map(([k, label]) => (
))}
Banner Halaman: {PAGE_LIST.find(p => p[0] === active)[1]}
Kosongkan untuk memakai teks bawaan. Tanpa foto, banner otomatis berhias ornamen brand.
upd('arabic', e.target.value)} style={dInp} placeholder="mis. اقرأ وتعلّم" />
upd('kicker', e.target.value)} style={dInp} placeholder="mis. Kumpulan Artikel" />
upd('title', e.target.value)} style={dInp} placeholder="Judul besar" />
upd('image', v)} />
{saving ? 'Menyimpan…' : 'Simpan Header'}
);
}
Object.assign(window, { ContentListModule, DonasiModule, SettingsModule, PageHeadersModule, KIND_SCHEMA });