'use client';
import React, { useState, useEffect, useRef } from 'react';
import { BookOpen, LayoutDashboard, Heart, Building2, CalendarDays, Image as ImageIcon, FileText, Settings, ShieldCheck, History, Landmark, DatabaseBackup, Users, LogOut, ExternalLink, Plus, Search, X, Check, Eye, Save, Upload, Trash2, Archive, LockKeyhole, RefreshCw, Clock3, ChevronUp, ChevronDown, Download, Loader2 } from 'lucide-react';
import { api, rupiah, tanggal, statusLabel, mediaSrc, Notice, Empty } from './site';
const roles: Record<string, string> = { owner: 'Pemilik', finance: 'Keuangan', editor: 'Editor' };
const publication: Record<string, string> = { draft: 'Draft', published: 'Diterbitkan', archived: 'Diarsipkan' };
export function Dialog({ title, children, onClose }: {
    title: string;
    children: React.ReactNode;
    onClose: () => void;
}) { const ref = useRef<HTMLDivElement>(null); useEffect(() => { const previous = document.activeElement as HTMLElement; const first = ref.current?.querySelector<HTMLElement>('button,input,select,textarea,a[href]'); first?.focus(); const key = (e: KeyboardEvent) => { if (e.key === 'Escape')
    onClose(); if (e.key === 'Tab') {
    const focusable = Array.from(ref.current?.querySelectorAll<HTMLElement>('button:not([disabled]),input:not([disabled]),select,textarea,a[href]') || []);
    const first = focusable[0], last = focusable[focusable.length - 1];
    if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last?.focus();
    }
    else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first?.focus();
    }
} }; document.addEventListener('keydown', key); const original = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', key); document.body.style.overflow = original; previous?.focus(); }; }, []); return <div className="modal-backdrop"><div className="dialog" ref={ref} role="dialog" aria-modal="true" aria-label={title}><div className="dialog-header"><h3>{title}</h3><button className="icon-button" onClick={onClose} aria-label="Tutup"><X size={21}/></button></div><div className="dialog-content">{children}</div></div></div>; }
function Preview({ record, payload, media, onClose }: {
    record: any;
    payload: any;
    media: any[];
    onClose: () => void;
}) { return <Dialog title="Pratinjau konten" onClose={onClose}><Notice>Pratinjau ini belum diterbitkan. Simpan lalu pilih Terbitkan untuk menampilkannya kepada publik.</Notice><div className="content-preview"><span className="eyebrow">{record.type === 'program' ? 'Program pembangunan' : record.type === 'progress' ? 'Pembaruan pembangunan' : 'Konten Al-Habibah'}</span><h2>{payload.title || payload.question || payload.name}</h2><p className="lead preline">{payload.description || payload.answer || payload.profile || payload.summary}</p>{payload.cover && <img src={mediaSrc(payload.cover)} alt={payload.title}/>}<p>{payload.benefit}</p>{payload.target && <strong>Target {rupiah(payload.target)}</strong>}{payload.budget?.map((b: any, i: number) => <p key={i}>{b.name}: {rupiah(b.amount)}</p>)}{payload.activityDate && <p>Tanggal kegiatan: {tanggal(payload.activityDate)}</p>}{payload.physicalPercent !== null && payload.physicalPercent !== undefined && <p>Progres fisik {payload.physicalPercent}%: {payload.physicalBasis}</p>}{payload.media?.map((id: string) => media.find(m => m.id === id)?.mime.startsWith('image/') ? <img key={id} src={mediaSrc(id)} alt={media.find(m => m.id === id)?.alt || ''}/> : null)}<p>{payload.policy}</p></div></Dialog>; }
function MediaSelect({ label, value, onChange, media, kind = 'image' }: {
    label: string;
    value?: string;
    onChange: (v: string) => void;
    media: any[];
    kind?: string;
}) { const list = media.filter(m => m.visibility === 'public' && (kind === 'pdf' ? m.mime === 'application/pdf' : m.mime.startsWith('image/'))); return <label>{label}<select value={value || ''} onChange={e => onChange(e.target.value)}><option value="">Belum dipilih</option>{list.map(m => <option key={m.id} value={m.id}>{m.caption || m.name}</option>)}</select>{value && kind === 'image' && <img className="field-image" src={mediaSrc(value)} alt={media.find(m => m.id === value)?.alt || label}/>}</label>; }
function GallerySelect({ ids, onChange, media }: {
    ids: string[];
    onChange: (v: string[]) => void;
    media: any[];
}) { return <div className="field"><label>Galeri dan urutan foto/video</label>{ids.map((id, i) => { const m = media.find(x => x.id === id); return <div className="selected-media" key={id}>{m?.mime.startsWith('image/') ? <img src={mediaSrc(id)} alt={m?.alt || m?.name}/> : <FileText size={20}/>}<span>{m?.caption || m?.name || 'File belum tersedia'}</span><button type="button" className="icon-button" aria-label="Geser ke atas" disabled={i === 0} onClick={() => { const next = [...ids]; [next[i - 1], next[i]] = [next[i], next[i - 1]]; onChange(next); }}><ChevronUp size={17}/></button><button type="button" className="icon-button" aria-label="Geser ke bawah" disabled={i === ids.length - 1} onClick={() => { const next = [...ids]; [next[i], next[i + 1]] = [next[i + 1], next[i]]; onChange(next); }}><ChevronDown size={17}/></button><button type="button" className="icon-button" aria-label="Lepaskan dari galeri" onClick={() => { if (window.confirm('Lepaskan dari galeri? File tetap tersimpan untuk riwayat versi.'))
    onChange(ids.filter(x => x !== id)); }}><X size={17}/></button></div>; })}<select value="" onChange={e => { if (e.target.value)
    onChange([...ids, e.target.value]); }}><option value="">Tambahkan foto/video dari galeri</option>{media.filter(m => m.visibility === 'public' && (m.mime.startsWith('image/') || m.mime === 'video/mp4') && !ids.includes(m.id)).map(m => <option key={m.id} value={m.id}>{m.caption || m.name}</option>)}</select><p className="field-help">Unggah file melalui menu Galeri. File yang dilepas tetap tersedia bagi versi sebelumnya.</p></div>; }
function RecordEditor({ record, data, onClose, onSaved }: {
    record: any;
    data: any;
    onClose: () => void;
    onSaved: () => void;
}) {
    const [p, setP] = useState<any>(JSON.parse(JSON.stringify(record.payload))), [summary, setSummary] = useState(''), [error, setError] = useState(''), [busy, setBusy] = useState(false), [preview, setPreview] = useState(false);
    const set = (k: string, v: any) => setP((p: any) => ({ ...p, [k]: v }));
    const field = (key: string, label: string, textarea = false, type = 'text') => <label key={key}>{label}{textarea ? <textarea rows={4} value={p[key] || ''} onChange={e => set(key, e.target.value)}/> : <input type={type} value={p[key] ?? ''} onChange={e => set(key, type === 'number' ? Number(e.target.value) : e.target.value)}/>}</label>;
    async function save(state: string) { if (state === 'archived' && !window.confirm('Arsipkan program? Riwayat donasi tetap utuh dan donasi baru ditutup.'))
        return; setBusy(true); setError(''); try {
        await api('admin/record', { id: record.id, type: record.type, revision: record.revision, payload: p, state, summary });
        onSaved();
        onClose();
    }
    catch (err) {
        setError((err as Error).message);
    }
    finally {
        setBusy(false);
    } }
    return <><Dialog title={record.id ? 'Ubah ' + (p.title || p.question || 'konten situs') : 'Tambah ' + ({ program: 'program', progress: 'progres', faq: 'FAQ', report: 'laporan' } as any)[record.type]} onClose={onClose}><form className="editor-form" onSubmit={e => { e.preventDefault(); save('draft'); }}>
 {record.type === 'program' && <><div className="form-grid">{field('title', 'Nama program')}{field('subtitle', 'Manfaat singkat')}{field('target', 'Target dana (rupiah)', false, 'number')}{field('order', 'Urutan program', false, 'number')}{p.icon==='land'&&<>{field('area','Luas lahan (m²)',false,'number')}{field('bata','Jumlah bata',false,'number')}</>}</div>{field('description', 'Deskripsi program', true)}{field('benefit', 'Manfaat bagi lembaga', true)}{field('illustration', 'Ilustrasi kontribusi, opsional', true)}<div className="field"><label>Rincian anggaran</label>{p.budget.map((b: any, i: number) => <div className="budget-row" key={i}><input aria-label="Kebutuhan" value={b.name} onChange={e => set('budget', p.budget.map((x: any, j: number) => j === i ? { ...x, name: e.target.value } : x))}/><input aria-label="Anggaran rupiah" type="number" min={1} value={b.amount} onChange={e => set('budget', p.budget.map((x: any, j: number) => j === i ? { ...x, amount: Number(e.target.value) } : x))}/><button className="icon-button" type="button" aria-label="Hapus baris anggaran" onClick={() => set('budget', p.budget.filter((_: any, j: number) => j !== i))}><X size={18}/></button></div>)}<button type="button" className="btn outline small" onClick={() => set('budget', [...p.budget, { name: '', amount: 0 }])}><Plus size={16}/>Tambah rincian</button><p className="field-help">Jumlah rincian {rupiah(p.budget.reduce((s: number, b: any) => s + Number(b.amount || 0), 0))}. Harus sama dengan target, tanpa menambahkan subtotal lagi.</p></div><MediaSelect label="Foto sampul" value={p.cover} onChange={v => set('cover', v || null)} media={data.media}/><GallerySelect ids={p.media || []} onChange={v => set('media', v)} media={data.media}/>{field('policy', 'Kebijakan dana ketika target terlampaui', true)}<label className="checkbox"><input type="checkbox" checked={p.open} onChange={e => set('open', e.target.checked)}/><span>Buka penerimaan donasi</span></label><label className="checkbox"><input type="checkbox" checked={p.main} onChange={e => set('main', e.target.checked)}/><span>Termasuk target kampanye utama</span></label></>}
 {record.type === 'progress' && <><div className="form-grid">{field('title', 'Judul pembaruan')}{field('activityDate', 'Tanggal kegiatan', false, 'date')}<label>Program<select value={p.programId || ''} onChange={e => set('programId', e.target.value || null)}><option value="">Pembangunan umum</option>{data.records.filter((x: any) => x.type === 'program').map((x: any) => <option key={x.id} value={x.id}>{x.payload.title}</option>)}</select></label><label>Persentase fisik, opsional<input type="number" min={0} max={100} value={p.physicalPercent ?? ''} onChange={e => set('physicalPercent', e.target.value === '' ? null : Number(e.target.value))}/></label></div>{field('description', 'Uraian pekerjaan', true)}{field('physicalBasis', 'Dasar penilaian persentase fisik', true)}<p className="field-help">Pisahkan pekerjaan fisik dari dana terkumpul. Jangan isi persentase tanpa penilaian pengelola.</p><GallerySelect ids={p.media || []} onChange={v => set('media', v)} media={data.media}/><div className="form-grid"><MediaSelect label="Foto sebelum, opsional" value={p.before} onChange={v => set('before', v || null)} media={data.media}/><MediaSelect label="Foto sesudah, opsional" value={p.after} onChange={v => set('after', v || null)} media={data.media}/></div></>}
 {record.type === 'faq' && <>{field('question', 'Pertanyaan')}{field('answer', 'Jawaban', true)}{field('order', 'Urutan', false, 'number')}</>}
 {record.type === 'report' && <>{field('title', 'Judul laporan')}<div className="form-grid">{field('from', 'Awal periode', false, 'date')}{field('to', 'Akhir periode', false, 'date')}</div><label>Cakupan program<select value={p.programId || ''} onChange={e => set('programId', e.target.value || null)}><option value="">Seluruh kampanye</option>{data.records.filter((x: any) => x.type === 'program').map((x: any) => <option key={x.id} value={x.id}>{x.payload.title}</option>)}</select></label>{field('summary', 'Ringkasan penggunaan dana yang aman untuk publik', true)}<MediaSelect label="PDF laporan yang telah disetujui" value={p.documentMedia} onChange={v => set('documentMedia', v || null)} kind="pdf" media={data.media}/><Notice>Angka laporan disimpan dari buku keuangan pada saat diterbitkan. Gunakan dokumen publik yang sudah menghapus kontak donatur, rekening pengirim, dan nota internal.</Notice></>}
 {record.type === 'site' && <><h4>Identitas & hero</h4><div className="form-grid">{field('name', 'Nama publik situs')}{field('foundation', 'Nama pengelola')}{field('pesantrenName','Nama pesantren')}{field('schoolName','Nama sekolah')}</div>{field('title', 'Judul hero', true)}{field('description', 'Deskripsi hero', true)}{field('heroKicker','Label atas hero')}{field('campaignTitle','Judul kartu kampanye',true)}{field('campaignLabel','Teks tombol kartu kampanye')}<div className="form-grid"><MediaSelect label="Foto hero asli" value={p.heroMedia} onChange={v => set('heroMedia', v || null)} media={data.media}/><MediaSelect label="Logo" value={p.logoMedia} onChange={v => set('logoMedia', v || null)} media={data.media}/>{field('primaryLabel', 'Teks tombol donasi')}{field('primaryHref', 'Tautan tombol donasi')}{field('secondaryLabel', 'Teks tombol kedua')}{field('secondaryHref', 'Tautan tombol kedua')}</div><h4>Judul bagian & halaman</h4>{field('programsTitle','Judul bagian program')}{field('progressTitle','Judul bagian progres')}{field('financeTitle','Judul bagian laporan')}{field('faqTitle','Judul bagian FAQ')}{field('closingTitle','Judul ajakan penutup')}{field('closingText','Teks ajakan penutup',true)}{field('aboutPageTitle','Judul halaman tentang')}{field('programsPageTitle','Judul halaman program')}{field('progressPageTitle','Judul halaman progres')}{field('reportsPageTitle','Judul halaman laporan')}{field('donationPageTitle','Judul halaman donasi')}<h4>Urutan bagian beranda</h4>{p.sectionOrder.map((s: string, i: number) => <div className="order-row" key={s}><span>{{ stats: 'Ringkasan dana', story: 'Cerita', programs: 'Program', progress: 'Progres', finance: 'Laporan', faq: 'FAQ' }[s] || s}</span><button type="button" className="icon-button" disabled={i === 0} aria-label="Geser bagian ke atas" onClick={() => { const list = [...p.sectionOrder]; [list[i - 1], list[i]] = [list[i], list[i - 1]]; set('sectionOrder', list); }}><ChevronUp size={17}/></button><button type="button" className="icon-button" disabled={i === p.sectionOrder.length - 1} aria-label="Geser bagian ke bawah" onClick={() => { const list = [...p.sectionOrder]; [list[i], list[i + 1]] = [list[i + 1], list[i]]; set('sectionOrder', list); }}><ChevronDown size={17}/></button></div>)}<h4>Cerita, profil & pengurus</h4>{field('storyTitle', 'Judul cerita')}{field('story', 'Cerita kebutuhan pembangunan', true)}{field('profile', 'Profil yayasan, pesantren, dan sekolah', true)}{field('vision', 'Visi yang telah dikonfirmasi', true)}{p.board.map((b: any, i: number) => <div className="budget-row" key={i}><input aria-label="Nama pengurus" placeholder="Nama yang dikonfirmasi" value={b.name} onChange={e => set('board', p.board.map((x: any, j: number) => j === i ? { ...x, name: e.target.value } : x))}/><input aria-label="Jabatan pengurus" placeholder="Jabatan" value={b.position} onChange={e => set('board', p.board.map((x: any, j: number) => j === i ? { ...x, position: e.target.value } : x))}/><button className="icon-button" type="button" aria-label="Hapus pengurus" onClick={() => set('board', p.board.filter((_: any, j: number) => j !== i))}><X size={17}/></button></div>)}<button type="button" className="btn outline small" onClick={() => set('board', [...p.board, { name: '', position: '' }])}>Tambah pengurus terkonfirmasi</button><h4>Kontak & dokumen</h4>{field('address', 'Alamat')}{field('whatsapp', 'WhatsApp dengan kode negara, contoh 6281546810836')}{field('website', 'Website lembaga')}{field('mapUrl', 'Tautan peta, isi setelah lokasi dikonfirmasi')}<MediaSelect label="Proposal PDF publik" value={p.proposalMedia} onChange={v => set('proposalMedia', v || null)} kind="pdf" media={data.media}/><div className="field"><label>Dokumen resmi publik</label>{p.officialDocuments?.map((id: string) => <div className="order-row" key={id}><span>{data.media.find((m: any) => m.id === id)?.name}</span><button className="icon-button" type="button" aria-label="Lepas dokumen" onClick={() => set('officialDocuments', p.officialDocuments.filter((x: string) => x !== id))}><X size={17}/></button></div>)}<select value="" onChange={e => { if (e.target.value)
        set('officialDocuments', [...(p.officialDocuments || []), e.target.value]); }}><option value="">Tambahkan dokumen PDF publik</option>{data.media.filter((m: any) => m.visibility === 'public' && m.mime === 'application/pdf' && !p.officialDocuments?.includes(m.id)).map((m: any) => <option key={m.id} value={m.id}>{m.name}</option>)}</select></div><h4>Ketentuan & privasi</h4>{field('allocationPolicy', 'Kebijakan penyaluran', true)}{field('verificationTime', 'Waktu layanan verifikasi yang telah dikonfirmasi', true)}{field('wakafExplanation', 'Penjelasan donasi pembangunan dan wakaf', true)}{field('privacy', 'Kebijakan privasi', true)}{field('terms', 'Ketentuan donasi', true)}{field('footer', 'Teks footer', true)}<h4>Informasi mesin pencari</h4>{field('seoTitle', 'Judul SEO')}{field('seoDescription', 'Deskripsi SEO', true)}<MediaSelect label="Gambar berbagi, opsional" value={p.shareMedia} onChange={v => set('shareMedia', v || null)} media={data.media}/></>}
 <label>Ringkasan perubahan<input value={summary} onChange={e => setSummary(e.target.value)} required placeholder="Jelaskan yang diubah, untuk riwayat versi" maxLength={500}/></label>{error && <Notice kind="error">{error}</Notice>}<div className="editor-actions"><button type="button" className="btn outline" onClick={() => setPreview(true)}><Eye size={17}/>Preview</button><button className="btn outline" disabled={busy}><Save size={17}/>{busy ? 'Menyimpan…' : 'Simpan draft'}</button><button type="button" className="btn" disabled={busy} onClick={() => { if (summary && window.confirm('Terbitkan konten ini ke halaman publik?'))
        save('published');
    else if (!summary)
        setError('Isi ringkasan perubahan terlebih dahulu.'); }}>Terbitkan</button>{record.type === 'program' && record.id && <button type="button" className="btn danger" disabled={busy} onClick={() => save('archived')}><Archive size={16}/>Arsipkan</button>}</div></form></Dialog>{preview && <Preview record={record} payload={p} media={data.media} onClose={() => setPreview(false)}/>}</>;
}
function AuthGate({ info, refresh }: {
    info: any;
    refresh: () => void;
}) {
    const [code, setCode] = useState(''), [recovery, setRecovery] = useState(false), [enrollment, setEnrollment] = useState<any>(null), [codes, setCodes] = useState<string[] | null>(null), [error, setError] = useState(''), [busy, setBusy] = useState(false);
    async function run(fn: () => Promise<any>) { setError(''); setBusy(true); try {
        await fn();
    }
    catch (err) {
        setError((err as Error).message);
    }
    finally {
        setBusy(false);
    } }
    return <div className="auth-shell"><a href="/" className="auth-brand"><BookOpen size={30}/>Donasi Al-Habibah</a><div className="auth-card"><span className="eyebrow">Ruang pengelola</span><h1>{codes ? 'Simpan kode pemulihan Anda.' : info?.admin ? 'Verifikasi dua langkah.' : info?.firstSetup ? 'Siapkan akun pemilik.' : 'Selamat datang kembali.'}</h1><p>{codes ? 'Kode hanya ditampilkan sekarang dan masing-masing dapat digunakan sekali. Simpan di tempat privat sebelum melanjutkan.' : info?.admin ? 'Authenticator menjaga akses ke konten dan keuangan Al-Habibah.' : 'Masuk menggunakan identitas yang terdaftar sebagai pengelola.'}</p>{!info ? <Empty title="Memeriksa akses…" icon={Loader2}/> : !info.configured ? <Notice>Keamanan admin belum dikonfigurasi. Pengelola teknis perlu memasang kunci enkripsi server.</Notice> : codes ? <><div className="recovery-codes">{codes.map(c => <code key={c}>{c}</code>)}</div><button className="btn full" onClick={refresh}>Saya sudah menyimpan, buka dashboard</button></> : !info.admin ? <>{info.signedIn ? <><div className="signed-identity"><ShieldCheck size={21}/><span>{info.email}</span></div>{info.firstSetup && <Notice>Pembuatan pemilik pertama tersedia pada situs privat ini. Faktor kedua wajib diaktifkan sebelum mengelola donasi.</Notice>}<button className="btn full" disabled={busy} onClick={() => run(async () => { await api('auth/start', {}); refresh(); })}>{busy ? 'Memeriksa…' : info.firstSetup ? 'Buat akun pemilik' : 'Masuk ke dashboard'}</button></> : <a className="btn full" href="/signin-with-chatgpt?return_to=%2Fadmin" target="_top">Masuk dengan ChatGPT</a>}</> : !info.admin.hasTotp ? <>{!enrollment ? <><Notice>Peran pemilik dan keuangan wajib mengaktifkan authenticator. Akses keuangan masih terkunci.</Notice><button className="btn full" disabled={busy} onClick={() => run(async () => setEnrollment(await api('auth/enroll', {})))}>Aktifkan authenticator</button></> : <><p>Tambahkan akun di aplikasi authenticator menggunakan kunci berikut. Pilih kode berdasarkan waktu.</p><code className="secret">{enrollment.secret}</code><a className="text-link" href={enrollment.uri}>Buka aplikasi authenticator</a><form onSubmit={e => { e.preventDefault(); run(async () => { const r = await api('auth/enroll-confirm', { code }); setCodes(r.codes); }); }}><label>Kode 6 digit<input inputMode="numeric" autoComplete="one-time-code" pattern="[0-9]{6}" maxLength={6} value={code} onChange={e => setCode(e.target.value)} required/></label><button className="btn full" disabled={busy}>Konfirmasi aktivasi</button></form></>}</> : <form onSubmit={e => { e.preventDefault(); run(async () => { await api('auth/verify', { code, recovery }); refresh(); }); }}><label>{recovery ? 'Kode pemulihan sekali pakai' : 'Kode authenticator'}<input autoComplete="one-time-code" value={code} onChange={e => setCode(e.target.value)} inputMode={recovery ? 'text' : 'numeric'} required placeholder={recovery ? 'Masukkan kode pemulihan' : '6 digit dari aplikasi'}/></label><button className="btn full" disabled={busy}>{busy ? 'Memeriksa…' : 'Verifikasi & lanjutkan'}</button><button type="button" className="text-link recovery-link" onClick={() => { setRecovery(!recovery); setCode(''); }}>{recovery ? 'Gunakan authenticator' : 'Authenticator hilang? Gunakan kode pemulihan'}</button></form>}{error && <Notice kind="error">{error}</Notice>}<div className="auth-foot"><LockKeyhole size={15}/>Identitas diperiksa di server. Tidak ada sandi admin bawaan.</div></div><a className="text-link" href="/">Kembali ke situs donasi</a></div>;
}
const navItems = [{ id: 'summary', name: 'Ringkasan', icon: LayoutDashboard, roles: ['owner', 'finance', 'editor'] }, { id: 'donations', name: 'Donasi', icon: Heart, roles: ['owner', 'finance'] }, { id: 'programs', name: 'Program', icon: Building2, roles: ['owner', 'editor'] }, { id: 'progress', name: 'Progres', icon: CalendarDays, roles: ['owner', 'editor'] }, { id: 'gallery', name: 'Galeri', icon: ImageIcon, roles: ['owner', 'finance', 'editor'] }, { id: 'reports', name: 'Laporan & keuangan', icon: FileText, roles: ['owner', 'finance'] }, { id: 'content', name: 'Konten', icon: BookOpen, roles: ['owner', 'editor'] }, { id: 'settings', name: 'Pengaturan', icon: Settings, roles: ['owner'] }, { id: 'security', name: 'Keamanan akun', icon: ShieldCheck, roles: ['owner', 'finance', 'editor'] }, { id: 'versions', name: 'Riwayat versi', icon: History, roles: ['owner', 'finance', 'editor'] }, { id: 'reconcile', name: 'Rekonsiliasi bank', icon: Landmark, roles: ['owner', 'finance'] }, { id: 'backups', name: 'Backup & pemulihan', icon: DatabaseBackup, roles: ['owner'] }, { id: 'admins', name: 'Akun & aktivitas', icon: Users, roles: ['owner'] }];
const initialPayload: Record<string, any> = { program: { title: '', subtitle: '', description: '', benefit: '', target: 0, budget: [{ name: '', amount: 0 }], media: [], cover: null, open: true, main: true, order: 6, policy: 'Dana tujuan khusus tidak dialihkan otomatis.', icon: 'building' }, progress: { title: '', activityDate: '', description: '', programId: null, physicalPercent: null, physicalBasis: '', media: [], before: null, after: null }, faq: { question: '', answer: '', order: 10 }, report: { title: '', summary: '', from: '', to: '', programId: null, documentMedia: null } };
function RecordList({ type, data, edit, create }: {
    type: string;
    data: any;
    edit: (r: any) => void;
    create: () => void;
}) { const records = data.records.filter((r: any) => r.type === type); return <div className="panel"><div className="panel-head"><h3>{{ program: 'Program pembangunan', progress: 'Pembaruan pembangunan', faq: 'Pertanyaan & jawaban', report: 'Laporan publik' }[type]}</h3><button className="btn small" onClick={create}><Plus size={16}/>Tambah</button></div>{records.length ? <div className="table-scroll"><table><thead><tr><th>Judul</th><th>Status</th><th>{type === 'program' ? 'Target' : 'Diperbarui'}</th><th>Versi</th><th /></tr></thead><tbody>{records.map((r: any) => <tr key={r.id}><td><strong>{r.payload.title || r.payload.question}</strong>{type === 'program' && <small>{r.payload.open ? 'Menerima donasi' : 'Donasi ditutup'}</small>}</td><td><span className={'tag ' + r.state}>{publication[r.state]}</span></td><td>{type === 'program' ? rupiah(r.payload.target) : tanggal(r.updated_at)}</td><td>{r.revision}</td><td><button className="btn outline small" onClick={() => edit(r)}>Ubah</button></td></tr>)}</tbody></table></div> : <Empty title="Belum ada konten">Tambahkan konten dan simpan draft untuk memeriksanya sebelum diterbitkan.</Empty>}</div>; }
function GalleryPanel({ data, refresh }: {
    data: any;
    refresh: () => void;
}) { const [files, setFiles] = useState<File[]>([]), [visibility, setVisibility] = useState('public'), [caption, setCaption] = useState(''), [alt, setAlt] = useState(''), [error, setError] = useState(''), [success, setSuccess] = useState(''), [busy, setBusy] = useState(false), [edit, setEdit] = useState<any>(null); async function upload(e: React.FormEvent) { e.preventDefault(); setBusy(true); setError(''); setSuccess(''); let saved = 0; try {
    for (const file of files) {
        const f = new FormData();
        f.set('file', file);
        f.set('visibility', visibility);
        f.set('caption', caption);
        f.set('alt', alt);
        const res = await fetch('/api/admin/upload', { method: 'POST', headers: { 'x-alhabibah': '1' }, body: f });
        const value: any = await res.json();
        if (!res.ok)
            throw new Error(value.error);
        saved++;
    }
    setSuccess(`${saved} file berhasil diunggah. Pilih file ini saat mengubah konten atau progres.`);
    setFiles([]);
    refresh();
}
catch (err) {
    setError(`${saved ? `${saved} file tersimpan. ` : ''}${(err as Error).message}`);
}
finally {
    setBusy(false);
} } return <><div className="panel"><div className="panel-head"><h3>Unggah dokumentasi & dokumen</h3></div><form className="upload-form" onSubmit={upload}><div className="form-grid"><label>Pilih beberapa file<input type="file" multiple accept="image/jpeg,image/png,application/pdf,video/mp4" onChange={e => setFiles(Array.from(e.target.files || []))} required/></label><label>Akses file<select value={visibility} onChange={e => setVisibility(e.target.value)}><option value="public">Publik, aman ditampilkan</option>{data.admin.role !== 'editor' && <option value="private">Privat, khusus keuangan</option>}</select></label></div><div className="form-grid"><label>Keterangan<input value={caption} onChange={e => setCaption(e.target.value)} placeholder="Keterangan foto atau dokumen"/></label><label>Alt text<input value={alt} onChange={e => setAlt(e.target.value)} placeholder="Jelaskan isi gambar untuk aksesibilitas"/></label></div><p className="field-help">JPG, PNG, PDF maksimal 5 MB. MP4 maksimal 25 MB. Jangan memilih publik untuk bukti transfer atau nota internal.</p>{files.length > 0 && <div className="pending-files">{files.map((f, i) => <span key={i}><FileText size={16}/>{f.name} · {(f.size / 1024 / 1024).toFixed(2)} MB</span>)}</div>}<button className="btn" disabled={busy || !files.length}><Upload size={17}/>{busy ? 'Mengunggah…' : 'Unggah & simpan'}</button>{error && <Notice kind="error">{error}</Notice>}{success && <Notice kind="success">{success}</Notice>}</form></div><div className="admin-gallery">{data.media.map((m: any) => <article key={m.id}>{m.mime.startsWith('image/') ? <img src={mediaSrc(m.id)} alt={m.alt || m.name} loading="lazy"/> : <div className="file-tile">{m.mime === 'video/mp4' ? <CalendarDays size={36}/> : <FileText size={36}/>}</div>}<div><span className="tag">{m.visibility === 'public' ? 'Publik' : 'Privat'}</span><strong>{m.caption || m.name}</strong><small>{m.alt || 'Alt text belum diisi'}</small><div className="gallery-actions"><a className="text-link" href={mediaSrc(m.id)} target="_blank" rel="noreferrer">Preview<ExternalLink size={14}/></a><button className="text-link" onClick={() => setEdit({ ...m })}>Ubah keterangan</button></div></div></article>)}</div>{edit && <Dialog title="Keterangan file" onClose={() => setEdit(null)}><form onSubmit={async (e) => { e.preventDefault(); try {
    await api('admin/media', { id: edit.id, caption: edit.caption, alt: edit.alt });
    setEdit(null);
    refresh();
}
catch (err) {
    setError((err as Error).message);
} }}><label>Keterangan<input value={edit.caption} onChange={e => setEdit({ ...edit, caption: e.target.value })}/></label><label>Alt text<input value={edit.alt} onChange={e => setEdit({ ...edit, alt: e.target.value })}/></label><button className="btn">Simpan keterangan</button></form></Dialog>}</>; }
function DonationsPanel({ data, perform, selected, setSelected }: {
    data: any;
    perform: (p: string, b: any) => Promise<boolean>;
    selected: any;
    setSelected: (d: any) => void;
}) {
    const [query, setQuery] = useState(''), [status, setStatus] = useState(''), [reason, setReason] = useState(''), [confirmed, setConfirmed] = useState(false), [bankId, setBankId] = useState(''), [offline, setOffline] = useState(false), [off, setOff] = useState({ name: '', amount: '', programId: '', reason: '' });
    const list = data.donations.filter((d: any) => (!status || d.status === status) && (!query || [d.name, d.id, d.amount].join(' ').toLowerCase().includes(query.toLowerCase())));
    const current = selected ? data.donations.find((d: any) => d.id === selected.id) : null;
    return <><div className="panel"><div className="panel-head"><h3>Donasi masuk</h3><div className="button-row"><button className="btn outline small" onClick={() => setOffline(true)}><Plus size={16}/>Penerimaan offline</button><a className="btn outline small" href="/api/admin/export"><Download size={16}/>Ekspor CSV</a></div></div><div className="table-filters"><label className="search-input"><Search size={17}/><input aria-label="Cari donasi" placeholder="Cari nama, nomor, atau nominal" value={query} onChange={e => setQuery(e.target.value)}/></label><select aria-label="Filter status" value={status} onChange={e => setStatus(e.target.value)}><option value="">Semua status</option>{Object.entries(statusLabel).map(([k, l]) => <option key={k} value={k}>{l}</option>)}</select></div>{list.length ? <div className="table-scroll"><table><thead><tr><th>Donatur & tanggal</th><th>Program</th><th>Nominal</th><th>Status</th><th /></tr></thead><tbody>{list.map((d: any) => <tr key={d.id}><td><strong>{d.name}</strong><small>{tanggal(d.created_at)} · {d.source === 'offline' ? 'Offline' : 'Website'}</small></td><td>{data.records.find((p: any) => p.id === d.program_id)?.payload.title || 'Dana prioritas'}</td><td className="nowrap">{rupiah(d.amount)}</td><td><span className={'tag ' + d.status}>{statusLabel[d.status]}</span></td><td><button className="btn outline small" onClick={() => { setSelected(d); setReason(''); setConfirmed(false); setBankId(''); }}>Periksa</button></td></tr>)}</tbody></table></div> : <Empty title="Belum ada donasi pada filter ini" icon={Heart}>Penerimaan bertambah setelah mutasi bank dicocokkan dan donasi diverifikasi.</Empty>}</div>{current && <Dialog title="Pemeriksaan donasi" onClose={() => setSelected(null)}><div className="donation-check"><span className={'tag ' + current.status}>{statusLabel[current.status]}</span><h2>{rupiah(current.amount)}</h2><p>{current.name} · {data.records.find((p: any) => p.id === current.program_id)?.payload.title || 'Dana prioritas'}</p><p className="mono">{current.id}</p>{current.contact && <p>Kontak privat: {current.contact}</p>}{current.message && <p>Pesan privat: {current.message}</p>}<h4>Bukti pembayaran privat</h4>{data.media.filter((m: any) => m.donation_id === current.id).length ? data.media.filter((m: any) => m.donation_id === current.id).map((m: any) => <a key={m.id} className="proof-link" href={mediaSrc(m.id)} target="_blank" rel="noreferrer"><FileText size={18}/>{m.name}<ExternalLink size={15}/></a>) : <Notice>Bukti belum diunggah. Penerimaan tetap harus diperiksa pada mutasi bank.</Notice>}<h4>Pencocokan bank</h4>{data.matches.filter((m: any) => m.donation_id === current.id).map((m: any) => <p key={m.id}><Check size={16}/> {rupiah(m.amount)} · {m.reason}</p>)}{!['verified', 'refunded'].includes(current.status) && <><label>Pilih kredit bank<select value={bankId} onChange={e => setBankId(e.target.value)}><option value="">Pilih mutasi yang telah diperiksa</option>{data.reconciliation.banks.filter((b: any) => b.credit > b.allocated && b.scope !== 'duplicate_excluded').map((b: any) => <option value={b.id} key={b.id}>{b.date} · {rupiah(b.credit - b.allocated)} tersedia · {b.description}</option>)}</select></label><p className="field-help">Impor mutasi melalui Rekonsiliasi bank. Nominal sama hanya saran awal. Periksa pengirim, waktu, dan referensi.</p><label>Alasan pemeriksaan<textarea rows={3} value={reason} onChange={e => setReason(e.target.value)} placeholder="Dasar pencocokan, referensi, dan konfirmasi peruntukan"/></label><label className="checkbox"><input type="checkbox" checked={confirmed} onChange={e => setConfirmed(e.target.checked)}/><span>Saya telah memeriksa asal dana dan peruntukan pada bank, bukan hanya kesamaan nominal.</span></label><div className="button-row"><button className="btn outline" disabled={!bankId || !confirmed} onClick={() => perform('admin/match', { bankId, donationId: current.id, amount: current.amount - data.matches.filter((m: any) => m.donation_id === current.id).reduce((s: number, m: any) => s + m.amount, 0), reason, confirmedSource: confirmed })}>Cocokkan penerimaan</button><button className="btn" disabled={!confirmed} onClick={async () => { if (await perform('admin/verify', { id: current.id, reason, confirmedSource: confirmed }))
        setSelected(null); }}>Verifikasi donasi</button><button className="btn danger" onClick={async () => { if (window.confirm('Tolak donasi ini dengan alasan yang telah diisi?') && await perform('admin/reject', { id: current.id, reason }))
        setSelected(null); }}>Tolak dengan alasan</button></div></>}{current.publish_message === 1 && <button className="btn outline small" onClick={() => perform('admin/moderate', { id: current.id, approved: !current.moderated })}>{current.moderated ? 'Sembunyikan pesan publik' : 'Setujui pesan untuk publik'}</button>}{['verified', 'refunded'].includes(current.status) && <Notice>Transaksi terverifikasi dikunci. Koreksi dan pengembalian dilakukan melalui catatan keuangan dengan alasan.</Notice>}</div></Dialog>}{offline && <Dialog title="Catat penerimaan offline" onClose={() => setOffline(false)}><form onSubmit={async (e) => { e.preventDefault(); if (await perform('admin/offline', { ...off, amount: Number(off.amount) }))
        setOffline(false); }}><Notice>Catatan awal menunggu verifikasi. Hubungkan ke mutasi bank yang sudah diperiksa agar tidak terhitung ganda.</Notice><label>Nama, opsional<input value={off.name} onChange={e => setOff({ ...off, name: e.target.value })}/></label><label>Nominal rupiah<input type="number" min={1} step={1} value={off.amount} onChange={e => setOff({ ...off, amount: e.target.value })} required/></label><label>Tujuan<select value={off.programId} onChange={e => setOff({ ...off, programId: e.target.value })}><option value="">Dana prioritas</option>{data.records.filter((r: any) => r.type === 'program').map((r: any) => <option key={r.id} value={r.id}>{r.payload.title}</option>)}</select></label><label>Asal dana & dasar peruntukan<textarea value={off.reason} onChange={e => setOff({ ...off, reason: e.target.value })} required minLength={8}/></label><button className="btn">Catat sebagai menunggu verifikasi</button></form></Dialog>}</>;
}
const ledgerLabels: Record<string, string> = { receipt: 'Penerimaan', expense: 'Pengeluaran', fee: 'Biaya transaksi', refund: 'Pengembalian dana', correction_credit: 'Koreksi pengeluaran / biaya', receipt_reversal: 'Pembalikan penerimaan', allocation: 'Alokasi dana prioritas', allocation_in: 'Alokasi masuk', allocation_out: 'Alokasi keluar' };
function FinancePanel({ data, perform, edit, create }: {
    data: any;
    perform: (p: string, b: any) => Promise<boolean>;
    edit: (r: any) => void;
    create: () => void;
}) { const [entry, setEntry] = useState(false), [f, setF] = useState({ kind: 'expense', amount: '', programId: '', description: '', category: '', date: '', donationId: '', relatedId: '', privateMediaId: '', requestKey: '' }); return <><RecordList type="report" data={data} edit={edit} create={create}/><div className="panel"><div className="panel-head"><h3>Buku penerimaan & penggunaan dana</h3><button className="btn small" onClick={() => { setF({ ...f, requestKey: crypto.randomUUID() }); setEntry(true); }}><Plus size={16}/>Catat penggunaan / koreksi</button></div>{data.ledger.length ? <div className="table-scroll"><table><thead><tr><th>Tanggal</th><th>Jenis & uraian</th><th>Program</th><th className="right">Nominal</th></tr></thead><tbody>{data.ledger.map((l: any) => <tr key={l.id}><td>{tanggal(l.date)}</td><td><strong>{ledgerLabels[l.kind]}</strong><small>{l.description}</small>{l.private_media_id && <a className="text-link" href={mediaSrc(l.private_media_id)} target="_blank" rel="noreferrer">Dokumen privat</a>}</td><td>{data.records.find((p: any) => p.id === l.program_id)?.payload.title || 'Dana prioritas'}</td><td className="right nowrap">{rupiah(l.amount)}</td></tr>)}</tbody></table></div> : <Empty title="Buku keuangan belum memiliki catatan">Penerimaan muncul setelah verifikasi. Catat penggunaan, biaya, serta pengembalian dana secara terpisah.</Empty>}</div>{entry && <Dialog title="Catatan keuangan" onClose={() => setEntry(false)}><form onSubmit={async (e) => { e.preventDefault(); if (await perform('admin/ledger', { ...f, amount: Number(f.amount) }))
    setEntry(false); }}><Notice>Catatan keuangan tidak dapat dihapus atau diubah diam-diam. Gunakan koreksi yang merujuk catatan awal. Alokasi hanya berasal dari dana prioritas.</Notice><label>Jenis catatan<select value={f.kind} onChange={e => setF({ ...f, kind: e.target.value })}>{['expense', 'fee', 'refund', 'correction_credit', 'receipt_reversal', 'allocation'].map(k => <option value={k} key={k}>{ledgerLabels[k]}</option>)}</select></label><div className="form-grid"><label>Nominal rupiah<input type="number" min={1} step={1} required value={f.amount} onChange={e => setF({ ...f, amount: e.target.value })}/></label><label>Tanggal pencatatan<input type="date" required value={f.date} onChange={e => setF({ ...f, date: e.target.value })}/></label></div><label>{f.kind === 'allocation' ? 'Program tujuan alokasi' : 'Kelompok dana'}<select value={f.programId} onChange={e => setF({ ...f, programId: e.target.value })}><option value="">Dana prioritas</option>{data.records.filter((r: any) => r.type === 'program').map((r: any) => <option value={r.id} key={r.id}>{r.payload.title}</option>)}</select></label>{['refund', 'receipt_reversal'].includes(f.kind) && <label>Donasi awal<select value={f.donationId} onChange={e => { const d = data.donations.find((d: any) => d.id === e.target.value); setF({ ...f, donationId: e.target.value, programId: d?.program_id || '' }); }} required><option value="">Pilih donasi terverifikasi</option>{data.donations.filter((d: any) => ['verified', 'refunded'].includes(d.status)).map((d: any) => <option value={d.id} key={d.id}>{d.name} · {rupiah(d.amount)} · {d.id.slice(0, 8)}</option>)}</select></label>}{f.kind === 'correction_credit' && <label>Catatan biaya / pengeluaran awal<select value={f.relatedId} required onChange={e => { const l = data.ledger.find((l: any) => l.id === e.target.value); setF({ ...f, relatedId: e.target.value, programId: l?.program_id || '' }); }}><option value="">Pilih catatan awal</option>{data.ledger.filter((l: any) => ['expense', 'fee'].includes(l.kind)).map((l: any) => <option key={l.id} value={l.id}>{rupiah(l.amount)} · {l.description}</option>)}</select></label>}<label>Kategori<input value={f.category} onChange={e => setF({ ...f, category: e.target.value })} placeholder="Contoh: bahan bangunan"/></label><label>Uraian & alasan<textarea value={f.description} onChange={e => setF({ ...f, description: e.target.value })} required minLength={8}/></label><label>Dokumen pendukung privat, opsional<select value={f.privateMediaId} onChange={e => setF({ ...f, privateMediaId: e.target.value })}><option value="">Belum dipilih</option>{data.media.filter((m: any) => m.visibility === 'private' && !m.donation_id).map((m: any) => <option key={m.id} value={m.id}>{m.name}</option>)}</select></label><button className="btn">Simpan catatan keuangan</button></form></Dialog>}</>; }
function ReconcilePanel({ data, perform }: {
    data: any;
    perform: (p: string, b: any) => Promise<boolean>;
}) {
    const [csv, setCsv] = useState<string[][]>([]), [fileName, setFileName] = useState(''), [mapping, setMapping] = useState<any>({ date: '', credit: '', debit: '', description: '', reference: '' }), [from, setFrom] = useState(''), [to, setTo] = useState(''), [error, setError] = useState(''), [preview, setPreview] = useState(false), [filter, setFilter] = useState('all'), [match, setMatch] = useState<any>(null), [donationId, setDonationId] = useState(''), [amount, setAmount] = useState(''), [reason, setReason] = useState(''), [confirmed, setConfirmed] = useState(false), [manual, setManual] = useState(false), [m, setM] = useState({ date: '', credit: '', debit: '', description: '', reference: '' }), [review, setReview] = useState<any>(null);
    const [periodRec, setPeriodRec] = useState<any>(null), [accountId, setAccountId] = useState('bsi');
    const historicalAccounts = (data.accounts || []).filter((a: any) => a.id !== 'bsi' && a.number !== data.bank?.number);
    useEffect(() => { setPeriodRec(null); }, [data]);
    const rec = periodRec || data.reconciliation;
    const columns = csv[0] || [];
    const mapped = csv.slice(1).map(row => Object.fromEntries(Object.entries(mapping).map(([key, index]) => [key, index === '' ? '' : row[Number(index)]?.trim() || ''])));
    const banks = rec.banks.filter((b: any) => (!from || b.date >= from) && (!to || b.date <= to) && (filter === 'all' || filter === 'unidentified' && b.scope === 'unidentified' || filter === 'partial' && b.scope === 'donation' && b.credit > b.allocated || filter === 'duplicate' && b.duplicate_candidate && !b.duplicate_resolved));
    async function read(file: File) { try {
        if (file.size > 2000000)
            throw new Error('CSV maksimal 2 MB.');
        const { parseCsv } = await import('@/lib/csv');
        const rows = parseCsv(await file.text());
        if (rows.length < 2 || rows.length > 501)
            throw new Error('CSV memerlukan header dan 1–500 baris data.');
        setCsv(rows);
        setFileName(file.name);
        setPreview(false);
        setError('');
        const cols = rows[0].map(x => x.toLowerCase());
        const find = (a: string[]) => { const i = cols.findIndex(x => a.includes(x)); return i < 0 ? '' : String(i); };
        setMapping({ date: find(['tanggal', 'date']), credit: find(['kredit', 'credit']), debit: find(['debit']), description: find(['uraian', 'description', 'keterangan']), reference: find(['referensi', 'reference']) });
    }
    catch (err) {
        setError((err as Error).message);
    } }
    return <><div className="reconcile-summary"><div><span>Penerimaan terverifikasi situs</span><strong>{rupiah(rec.siteReceipts)}</strong></div><div><span>Kredit bank yang dialokasikan</span><strong>{rupiah(rec.matchedBankCredits)}</strong></div><div><span>Selisih cakupan</span><strong>{rupiah(rec.difference)}</strong></div></div><Notice>{rec.scope} {periodRec ? 'Ringkasan mengikuti rentang tanggal yang ditampilkan.' : 'Ringkasan mencakup semua tanggal; isi periode untuk membandingkan rentang tertentu.'}</Notice><div className="panel"><div className="panel-head"><h3>Impor mutasi BSI</h3><button className="btn outline small" onClick={() => setManual(true)}><Plus size={16}/>Input manual</button></div><div className="upload-form"><p>Impor CSV mutasi rekening terkonfirmasi. Data mutasi bersifat privat. Website tidak meminta sandi internet banking.</p><div className="form-grid"><label>CSV dengan header<input type="file" accept=".csv,text/csv" onChange={e => { if (e.target.files?.[0])
        read(e.target.files[0]); }}/></label><label>Rekening untuk impor<select value={accountId} onChange={e => setAccountId(e.target.value)}><option value="bsi">{data.bank?.bank} · {data.bank?.number} (aktif)</option>{historicalAccounts.map((a: any) => <option key={a.id} value={a.id} disabled={!a.confirmed}>{a.bank} · {a.number}{a.confirmed ? ' (riwayat)' : ' (belum dikonfirmasi)'}</option>)}</select></label><label>Awal rentang<input type="date" value={from} onChange={e => setFrom(e.target.value)}/></label><label>Akhir rentang<input type="date" value={to} min={from || undefined} onChange={e => setTo(e.target.value)}/></label></div><button className="btn outline small" onClick={async () => { try {
        setPeriodRec(await api('admin/reconciliation?' + new URLSearchParams({ from: from || '0000', to: to || '9999' })));
        setError('');
    }
    catch (err) {
        setError((err as Error).message);
    } }}>Bandingkan periode</button>{csv.length > 0 && <><div className="form-grid">{Object.entries({ date: 'Tanggal (YYYY-MM-DD)', credit: 'Kredit rupiah', debit: 'Debit rupiah', description: 'Uraian', reference: 'Referensi bank, opsional' }).map(([key, label]) => <label key={key}>{label}<select value={mapping[key]} onChange={e => setMapping({ ...mapping, [key]: e.target.value })}><option value="">Tidak dipetakan</option>{columns.map((col, i) => <option value={i} key={i}>{col}</option>)}</select></label>)}</div><p className="field-help">Nominal memakai angka rupiah tanpa titik atau koma. Tanggal memakai YYYY-MM-DD. Pilih salah satu kredit atau debit yang positif pada setiap baris.</p><button className="btn outline" onClick={() => setPreview(true)}><Eye size={16}/>Preview {mapped.length} baris</button>{preview && <><div className="table-scroll"><table><thead><tr><th>Tanggal</th><th>Kredit</th><th>Debit</th><th>Uraian</th><th>Referensi</th></tr></thead><tbody>{mapped.slice(0, 10).map((r, i) => <tr key={i}><td>{r.date}</td><td>{r.credit}</td><td>{r.debit}</td><td>{r.description}</td><td>{r.reference || 'Tanpa referensi'}</td></tr>)}</tbody></table></div><Notice>Referensi bank yang sudah ada tidak dimasukkan ulang. Tanpa referensi, kandidat duplikat ditandai untuk diperiksa. Impor mutasi tidak otomatis menjadi donasi.</Notice><button className="btn" onClick={async () => { if (await perform('admin/import', { accountId, source: fileName, from, to, rows: mapped })) {
        setCsv([]);
        setPreview(false);
    } }}>Konfirmasi & impor</button></>}</>}{error && <Notice kind="error">{error}</Notice>}</div></div><div className="panel"><div className="panel-head"><h3>Antrean pencocokan</h3><select aria-label="Filter pencocokan" value={filter} onChange={e => setFilter(e.target.value)}><option value="all">Semua mutasi</option><option value="unidentified">Belum teridentifikasi</option><option value="partial">Parsial / berlebih</option><option value="duplicate">Dugaan duplikat</option></select></div>{banks.length ? <div className="table-scroll"><table><thead><tr><th>Tanggal & uraian</th><th>Kredit / debit</th><th>Tersedia</th><th>Cakupan</th><th /></tr></thead><tbody>{banks.map((b: any) => <tr key={b.id}><td><strong>{b.date}</strong><small>{b.description}</small><small>Rekening {(data.accounts || []).find((a: any) => a.id === b.account_id)?.number || b.account_id}</small><small>{b.reference || 'Tanpa referensi'}{b.duplicate_candidate && !b.duplicate_resolved ? ' · Dugaan duplikat' : ''}</small></td><td>{rupiah(b.credit || b.debit)}<small>{b.credit ? 'Kredit' : 'Debit'}</small></td><td>{rupiah(Math.max(0, b.credit - b.allocated))}</td><td>{({ donation: 'Terkait donasi', other: 'Di luar kampanye', unidentified: 'Belum dikenali', duplicate_excluded: 'Duplikat dikecualikan' } as Record<string, string>)[b.scope]}</td><td><div className="button-row">{b.credit > b.allocated && b.scope !== 'duplicate_excluded' && <button className="btn small outline" onClick={() => { setMatch(b); setDonationId(''); setAmount(''); setReason(''); setConfirmed(false); }}>Cocokkan</button>}<button className="btn small outline" onClick={() => { setReview({ ...b, newScope: b.scope, reason: '' }); }}>Periksa</button></div></td></tr>)}</tbody></table></div> : <Empty title="Mutasi bank belum tersedia pada filter ini" icon={Landmark}>Impor CSV atau masukkan mutasi secara manual untuk mulai mencocokkan.</Empty>}</div>{data.matches.length > 0 && <div className="panel"><div className="panel-head"><h3>Riwayat pencocokan</h3></div><div className="table-scroll"><table><thead><tr><th>Donasi</th><th>Jumlah</th><th>Dasar pencocokan</th><th /></tr></thead><tbody>{data.matches.map((x: any) => <tr key={x.id}><td>{data.donations.find((d: any) => d.id === x.donation_id)?.name || x.donation_id}</td><td>{rupiah(x.amount)}</td><td>{x.reason}</td><td>{!['verified', 'refunded'].includes(data.donations.find((d: any) => d.id === x.donation_id)?.status) && <button className="text-link" onClick={() => { const reason = window.prompt('Alasan melepas pencocokan, minimal 8 karakter:'); if (reason)
        perform('admin/unmatch', { id: x.id, reason }); }}>Lepas dengan alasan</button>}</td></tr>)}</tbody></table></div></div>}{match && <Dialog title="Cocokkan kredit bank" onClose={() => setMatch(null)}><form onSubmit={async (e) => { e.preventDefault(); if (await perform('admin/match', { bankId: match.id, donationId, amount: Number(amount), reason, confirmedSource: confirmed }))
        setMatch(null); }}><p>{match.date} · {match.description}<br />Dana tersedia {rupiah(match.credit - match.allocated)}</p><label>Permintaan donasi<select value={donationId} onChange={e => { const d = data.donations.find((d: any) => d.id === e.target.value); const allocated = data.matches.filter((m: any) => m.donation_id === d?.id).reduce((s: number, m: any) => s + m.amount, 0); setDonationId(e.target.value); setAmount(String(Math.min((d?.amount || 0) - allocated, match.credit - match.allocated))); }} required><option value="">Pilih donasi yang telah diperiksa</option>{data.donations.filter((d: any) => !['verified', 'refunded', 'rejected'].includes(d.status)).sort((a: any, b: any) => Math.abs(a.amount - match.credit) - Math.abs(b.amount - match.credit)).map((d: any) => <option key={d.id} value={d.id}>{rupiah(d.amount)} · {d.name} · {d.id.slice(0, 8)}</option>)}</select></label><label>Jumlah dialokasikan<input type="number" min={1} value={amount} onChange={e => setAmount(e.target.value)} required/></label><label>Dasar identifikasi & peruntukan<textarea value={reason} onChange={e => setReason(e.target.value)} required minLength={8}/></label><label className="checkbox"><input type="checkbox" checked={confirmed} onChange={e => setConfirmed(e.target.checked)} required/><span>Saya memeriksa sumber dana, waktu, dan referensi. Nominal sama saja tidak cukup.</span></label><button className="btn">Simpan pencocokan</button></form></Dialog>}{manual && <Dialog title="Input mutasi manual" onClose={() => setManual(false)}><form onSubmit={async (e) => { e.preventDefault(); if (await perform('admin/import', { accountId, source: 'Input manual', from: m.date, to: m.date, rows: [m] }))
        setManual(false); }}><label>Tanggal<input type="date" value={m.date} required onChange={e => setM({ ...m, date: e.target.value })}/></label><div className="form-grid"><label>Kredit rupiah<input type="number" min={0} step={1} value={m.credit} onChange={e => setM({ ...m, credit: e.target.value })}/></label><label>Debit rupiah<input type="number" min={0} step={1} value={m.debit} onChange={e => setM({ ...m, debit: e.target.value })}/></label></div><label>Uraian<input value={m.description} required onChange={e => setM({ ...m, description: e.target.value })}/></label><label>Referensi bank, jika tersedia<input value={m.reference} onChange={e => setM({ ...m, reference: e.target.value })}/></label><button className="btn">Simpan mutasi</button></form></Dialog>}{review && <Dialog title="Pemeriksaan cakupan mutasi" onClose={() => setReview(null)}><form onSubmit={async (e) => { e.preventDefault(); if (await perform('admin/bank-review', { id: review.id, scope: review.newScope, reason: review.reason }))
        setReview(null); }}><p>{review.description} · {rupiah(review.credit || review.debit)}</p><label>Cakupan<select value={review.newScope} onChange={e => setReview({ ...review, newScope: e.target.value })}><option value="unidentified">Belum teridentifikasi</option><option value="donation">Terkait donasi, tetap perlu pencocokan</option><option value="other">Di luar kampanye</option><option value="duplicate_excluded">Duplikat yang telah diperiksa</option></select></label><label>Alasan hasil pemeriksaan<textarea value={review.reason} onChange={e => setReview({ ...review, reason: e.target.value })} required minLength={8}/></label><button className="btn">Simpan hasil pemeriksaan</button></form></Dialog>}</>;
}
function VersionsPanel({ data, perform, edit }: {
    data: any;
    perform: (p: string, b: any) => Promise<boolean>;
    edit: (r: any) => void;
}) { const [id, setId] = useState(''), [versions, setVersions] = useState<any[]>([]), [selected, setSelected] = useState<any>(null), [error, setError] = useState(''), [preview, setPreview] = useState(false); const record = data.records.find((r: any) => r.id === id); const available = data.records.filter((r: any) => data.admin.role === 'finance' ? r.type === 'report' : data.admin.role === 'editor' ? r.type !== 'report' : true); useEffect(() => { if (id)
    api('admin/versions?id=' + id).then(setVersions).catch(e => setError(e.message));
else
    setVersions([]); setSelected(null); }, [id, data]); return <><div className="panel"><div className="panel-head"><h3>Versi konten</h3></div><div className="upload-form"><label>Pilih konten<select value={id} onChange={e => setId(e.target.value)}><option value="">Pilih hero, profil, FAQ, program, atau progres</option>{available.map((r: any) => <option key={r.id} value={r.id}>{r.payload.title || r.payload.question || r.payload.name}</option>)}</select></label><Notice>Pemulihan membuat draft versi baru. Saldo, pembayaran, dan rekening aktif tidak ikut dipulihkan.</Notice>{error && <Notice kind="error">{error}</Notice>}</div>{versions.length > 0 && <div className="table-scroll"><table><thead><tr><th>Versi</th><th>Waktu & pelaku</th><th>Perubahan</th><th>Status</th><th /></tr></thead><tbody>{versions.map(v => <tr key={v.id}><td>v{v.revision}</td><td>{tanggal(v.created_at)}<small>{v.actor_name || 'Data awal'}</small></td><td>{v.summary}</td><td>{publication[v.state]}</td><td><button className="btn outline small" onClick={() => setSelected({ ...v, snapshot: JSON.parse(v.snapshot) })}>Bandingkan</button></td></tr>)}</tbody></table></div>}</div>{selected && record && <div className="panel"><div className="panel-head"><h3>Versi {selected.revision} dibandingkan versi {record.revision}</h3><button className="btn outline small" onClick={() => setPreview(true)}><Eye size={16}/>Preview versi lama</button></div><div className="table-scroll"><table><thead><tr><th>Bagian</th><th>Versi lama</th><th>Versi sekarang</th></tr></thead><tbody>{Object.keys({ ...selected.snapshot, ...record.payload }).filter(k => JSON.stringify(selected.snapshot[k]) !== JSON.stringify(record.payload[k])).map(k => <tr key={k}><td>{k}</td><td className="diff-cell">{typeof selected.snapshot[k] === 'object' ? JSON.stringify(selected.snapshot[k], null, 2) : String(selected.snapshot[k] ?? '')}</td><td className="diff-cell">{typeof record.payload[k] === 'object' ? JSON.stringify(record.payload[k], null, 2) : String(record.payload[k] ?? '')}</td></tr>)}</tbody></table></div><div className="upload-form"><button className="btn" onClick={async () => { const reason = window.prompt('Alasan memulihkan versi ini sebagai draft baru:'); if (reason && await perform('admin/restore-version', { versionId: selected.id, revision: record.revision, reason })) {
    setSelected(null);
} }}><History size={17}/>Pulihkan sebagai draft baru</button><button className="btn outline" onClick={() => edit(record)}>Buka editor</button></div></div>}{preview && selected && record && <Preview record={record} payload={selected.snapshot} media={data.media} onClose={() => setPreview(false)}/>}</>; }
function SecurityPanel({ data, refresh }: {
    data: any;
    refresh: () => void;
}) { const [recovery, setRecovery] = useState(false), [code, setCode] = useState(''), [error, setError] = useState(''), [success, setSuccess] = useState(''), [enrollment, setEnrollment] = useState<any>(null), [codes, setCodes] = useState<string[] | null>(null), [busy, setBusy] = useState(false); async function run(fn: () => Promise<void>) { setBusy(true); setError(''); try {
    await fn();
}
catch (err) {
    setError((err as Error).message);
}
finally {
    setBusy(false);
} } return <div className="panel security-panel"><div className="panel-head"><h3>Keamanan akun Anda</h3><ShieldCheck size={22}/></div><div className="upload-form"><div className="security-state"><ShieldCheck size={32}/><div><strong>{data.admin.hasTotp ? 'Authenticator aktif' : 'Authenticator belum aktif'}</strong><p>{roles[data.admin.role]} · {data.admin.email}</p></div></div><p>Login identitas dikelola melalui ChatGPT. Faktor kedua aplikasi diperiksa oleh server. Pemilik dan keuangan wajib menggunakan authenticator.</p><form onSubmit={e => { e.preventDefault(); run(async () => { await api('auth/verify', { code, reauth: true, recovery }); setSuccess('Autentikasi ulang berhasil. Tindakan sensitif diizinkan selama 5 menit.'); setCode(''); }); }}><label>{recovery ? 'Kode pemulihan lain untuk autentikasi ulang' : 'Kode authenticator untuk autentikasi ulang'}<input value={code} onChange={e => setCode(e.target.value)} autoComplete="one-time-code" inputMode={recovery ? 'text' : 'numeric'} maxLength={recovery ? 32 : 6} required/></label><button className="btn" disabled={busy || !data.admin.hasTotp}>Verifikasi ulang</button><button type="button" className="text-link recovery-link" onClick={() => { setRecovery(!recovery); setCode(''); }}>{recovery ? 'Gunakan authenticator' : 'Perangkat hilang? Gunakan kode pemulihan yang belum dipakai'}</button></form><p className="field-help">Verifikasi ulang diperlukan sebelum mengganti rekening, mengelola admin, mengganti faktor kedua, atau memulihkan data. Kode yang sudah dipakai tidak dapat digunakan ulang. Tunggu kode berikutnya.</p><h4 className="spaced">Ganti / aktifkan authenticator</h4><p>Saat kehilangan perangkat, masuk dengan kode pemulihan sekali pakai. Penggantian authenticator memerlukan verifikasi ulang. Tanpa faktor aktif atau kode pemulihan, pemilik harus memulihkan akses melalui prosedur operator yang tercatat.</p><button className="btn outline" disabled={busy} onClick={() => run(async () => { setEnrollment(await api('auth/enroll', {})); setCodes(null); })}>Mulai pengaturan authenticator</button>{enrollment && <><code className="secret">{enrollment.secret}</code><a className="text-link" href={enrollment.uri}>Buka aplikasi authenticator</a><form onSubmit={e => { e.preventDefault(); run(async () => { const r = await api('auth/enroll-confirm', { code }); setCodes(r.codes); setEnrollment(null); setSuccess('Authenticator diaktifkan. Simpan kode pemulihan yang baru.'); refresh(); }); }}><label>Kode dari authenticator baru<input value={code} onChange={e => setCode(e.target.value)} inputMode="numeric" maxLength={6} required/></label><button className="btn" disabled={busy}>Konfirmasi faktor kedua baru</button></form></>}{codes && <><Notice>Kode hanya ditampilkan sekarang. Kode sebelumnya sudah tidak berlaku. Simpan secara privat.</Notice><div className="recovery-codes">{codes.map(c => <code key={c}>{c}</code>)}</div><button className="btn outline" onClick={() => setCodes(null)}>Saya sudah menyimpan kode</button></>}{data.admin.role === 'editor' && data.admin.hasTotp && <button className="btn danger" onClick={() => run(async () => { if (window.confirm('Nonaktifkan faktor kedua editor? Verifikasi ulang diperlukan.')) {
    await api('auth/disable', {});
    refresh();
} })}>Nonaktifkan faktor kedua editor</button>}{success && <Notice kind="success">{success}</Notice>}{error && <Notice kind="error">{error}</Notice>}</div></div>; }
function SettingsPanel({ data, perform }: {
    data: any;
    perform: (p: string, b: any) => Promise<boolean>;
}) { const [b, setB] = useState({ ...data.bank, sourceMediaId: data.bank.source_media_id || '', reason: '', confirmed: false, makeActive: true }); return <><div className="panel"><div className="panel-head"><h3>Rekening penerimaan donasi</h3><span className="tag">{data.bank.confirmed ? 'Terkonfirmasi' : 'Belum dikonfirmasi'}</span></div><form className="upload-form" onSubmit={e => { e.preventDefault(); if (window.confirm('Simpan identitas rekening berdasarkan dokumen yang sudah diperiksa? Perubahan akan dicatat.'))
    perform('admin/bank-settings', b); }}><Notice>Hanya pemilik dapat mengganti rekening. Lakukan autentikasi ulang pada menu Keamanan akun, lalu periksa dokumen sumber sebelum mengaktifkan instruksi pembayaran.</Notice><div className="form-grid"><label>Bank<input value={b.bank} onChange={e => setB({ ...b, bank: e.target.value })} required/></label><label>Nomor rekening<input inputMode="numeric" pattern="[0-9]{6,30}" value={b.number} onChange={e => setB({ ...b, number: e.target.value.replace(/\D/g, '') })} required/></label></div><label>Atas nama<input value={b.holder} onChange={e => setB({ ...b, holder: e.target.value })} required/></label><label>Dokumen sumber PDF<select value={b.sourceMediaId} onChange={e => setB({ ...b, sourceMediaId: e.target.value })} required><option value="">Pilih dokumen yang telah diunggah</option>{data.media.filter((m: any) => m.mime === 'application/pdf').map((m: any) => <option key={m.id} value={m.id}>{m.name} ({m.visibility === 'private' ? 'privat' : 'publik'})</option>)}</select></label><label>Dasar konfirmasi & alasan perubahan<textarea value={b.reason} onChange={e => setB({ ...b, reason: e.target.value })} required minLength={8}/></label><label className="checkbox"><input type="checkbox" checked={b.confirmed} onChange={e => setB({ ...b, confirmed: e.target.checked })} required/><span>Saya telah mencocokkan nomor rekening dan nama penerima dengan dokumen sumber.</span></label><label className="checkbox"><input type="checkbox" checked={b.makeActive} onChange={e => setB({ ...b, makeActive: e.target.checked })}/><span>Gunakan sebagai rekening aktif untuk donasi baru. Nonaktifkan pilihan ini untuk mengonfirmasi kembali rekening riwayat.</span></label><button className="btn">Konfirmasi & simpan rekening</button></form></div><div className="panel"><div className="panel-head"><h3>Metode pembayaran otomatis</h3><span className="tag">Belum aktif</span></div><div className="upload-form"><p>QRIS dan virtual account memerlukan akun merchant serta integrasi resmi. Metode ini belum tersedia. Transfer manual menjadi jalur utama setelah rekening dikonfirmasi.</p><p>Identitas situs, WhatsApp, logo, SEO, dan kebijakan dapat diubah melalui menu Konten.</p></div></div></>; }
function BackupPanel({ data, perform }: {
    data: any;
    perform: (p: string, b: any) => Promise<boolean>;
}) { const settings = data.backupSettings ? JSON.parse(data.backupSettings.value) : { retention: 30, time: '02:00' }; const [f, setF] = useState(settings), [preview, setPreview] = useState<any>(null), [error, setError] = useState(''), [reason, setReason] = useState(''), [confirmation, setConfirmation] = useState(''); const last = data.backups.find((b: any) => b.status === 'succeeded'); return <><div className="panel"><div className="panel-head"><h3>Backup otomatis harian</h3><span className="tag">{data.backupConfigured && data.backupScheduleActive ? 'Aktif' : 'Belum aktif'}</span></div><div className="upload-form">{!data.backupConfigured || !data.backupScheduleActive ? <Notice>Hubungkan penyimpanan cadangan terpisah, kunci enkripsi, dan penjadwal melalui panduan pemasangan. Status belum aktif sampai konfigurasi dan pemanggilan terjadwal diverifikasi.</Notice> : <Notice kind="success">Backup terjadwal dikonfigurasi. Periksa riwayat untuk memastikan pekerjaan berhasil.</Notice>}<p>Backup mencakup database, foto, proposal, dokumen privat, hak akses, dan riwayat versi. Cadangan disimpan terenkripsi di tempat terpisah.</p><form onSubmit={e => { e.preventDefault(); perform('admin/backup-settings', f); }}><div className="form-grid"><label>Waktu harian (WIB)<input type="time" value={f.time} onChange={e => setF({ ...f, time: e.target.value })} required/></label><label>Masa simpan (hari)<input type="number" min={7} max={365} value={f.retention} onChange={e => setF({ ...f, retention: Number(e.target.value) })} required/></label></div><button className="btn outline">Simpan jadwal & masa simpan</button></form><p>Backup terakhir berhasil: <strong>{last ? tanggal(last.finished_at) : 'Belum ada'}</strong></p><button className="btn" disabled={!data.backupConfigured} onClick={() => perform('admin/backup', {})}><DatabaseBackup size={17}/>Jalankan backup sekarang</button><p className="field-help">Backup manual dan pemulihan memerlukan autentikasi ulang. Kegagalan ditampilkan di sini. Notifikasi eksternal belum dikonfigurasi.</p></div></div><div className="panel"><div className="panel-head"><h3>Riwayat backup</h3></div>{data.backups.length ? <div className="table-scroll"><table><thead><tr><th>Mulai / selesai</th><th>Cakupan</th><th>Status</th><th>Masa simpan</th><th /></tr></thead><tbody>{data.backups.map((b: any) => <tr key={b.id}><td>{tanggal(b.started_at)}<small>{b.finished_at ? tanggal(b.finished_at) : 'Belum selesai'}</small></td><td>{b.scope}<small>{b.error}</small></td><td><span className={'tag ' + b.status}>{b.status === 'succeeded' ? 'Berhasil' : b.status === 'failed' ? 'Gagal' : b.status === 'partial' ? 'Checkpoint sebagian' : 'Berjalan'}</span></td><td>{tanggal(b.expires_at)}</td><td>{b.status === 'succeeded' && <button className="btn outline small" onClick={async () => { try {
    setPreview(await api('admin/restore-preview', { id: b.id }));
    setError('');
}
catch (err) {
    setError((err as Error).message);
} }}>Periksa pemulihan</button>}</td></tr>)}</tbody></table></div> : <Empty title="Belum ada pekerjaan backup" icon={DatabaseBackup}>Riwayat akan tersedia setelah layanan cadangan diaktifkan.</Empty>}</div><div className="panel"><div className="panel-head"><h3>Panduan pemulihan</h3></div><div className="upload-form"><ol><li>Pastikan cadangan berhasil dan kunci enkripsi tersedia.</li><li>Uji pemulihan penuh di database dan penyimpanan file yang terpisah.</li><li>Periksa transaksi, saldo per program, dokumen, foto, dan peran admin.</li><li>Untuk produksi, lakukan autentikasi ulang, periksa cakupan, dan cadangkan kondisi saat ini.</li><li>Pemulihan dashboard menambahkan data yang hilang sambil mempertahankan data aktif. Pemulihan penuh dilakukan operator sesuai panduan source code.</li><li>Rekonsiliasi pembayaran setelah waktu backup sebelum membuka layanan.</li></ol></div></div>{data.restores.length > 0 && <div className="panel"><div className="panel-head"><h3>Riwayat pemulihan</h3></div><div className="upload-form">{data.restores.map((r: any) => <p key={r.id}><strong>{tanggal(r.created_at)} · {r.status}</strong><br />{r.detail}</p>)}</div></div>}{error && <Notice kind="error">{error}</Notice>}{preview && <Dialog title="Periksa cakupan pemulihan" onClose={() => setPreview(null)}><Notice>{preview.mode}</Notice><p>Waktu backup: {tanggal(preview.startedAt)}<br />Jumlah file: {preview.files}<br />Catatan keuangan setelah backup: {preview.laterFinancialEntries}</p><div className="table-scroll"><table><tbody>{Object.entries(preview.counts).map(([k, v]) => <tr key={k}><td>{k}</td><td>{String(v)} baris</td></tr>)}</tbody></table></div><form onSubmit={async (e) => { e.preventDefault(); if (await perform('admin/restore', { id: preview.id, reason, confirmation }))
    setPreview(null); }}><label>Alasan pemulihan<textarea value={reason} required minLength={12} onChange={e => setReason(e.target.value)}/></label><label>Ketik PULIHKAN DATA<input value={confirmation} onChange={e => setConfirmation(e.target.value)} required/></label><button className="btn danger">Cadangkan kondisi sekarang & pulihkan</button></form></Dialog>}</>; }
function AccountsPanel({ data, perform }: {
    data: any;
    perform: (p: string, b: any) => Promise<boolean>;
}) { const [f, setF] = useState({ email: '', role: 'editor' }), [edit, setEdit] = useState<any>(null); return <><div className="panel"><div className="panel-head"><h3>Admin & hak akses</h3></div><div className="upload-form"><Notice>Pengelolaan akun memerlukan autentikasi ulang pada Keamanan akun. Pemilik dan keuangan wajib menggunakan faktor kedua.</Notice><form onSubmit={async (e) => { e.preventDefault(); if (await perform('admin/admins', f))
    setF({ email: '', role: 'editor' }); }} className="inline-form"><label>Email akun ChatGPT<input type="email" value={f.email} required onChange={e => setF({ ...f, email: e.target.value })}/></label><label>Peran<select value={f.role} onChange={e => setF({ ...f, role: e.target.value })}><option value="editor">Editor</option><option value="finance">Keuangan</option><option value="owner">Pemilik</option></select></label><button className="btn"><Plus size={16}/>Izinkan akun</button></form><p className="field-help">Pengguna membuka halaman admin dan masuk dengan akun yang diizinkan. Website tidak mengirim undangan email otomatis.</p></div><div className="table-scroll"><table><thead><tr><th>Akun</th><th>Peran</th><th>Authenticator</th><th>Status</th><th /></tr></thead><tbody>{data.admins.map((a: any) => <tr key={a.id}><td>{a.name}<small>{a.email}</small></td><td>{roles[a.role]}</td><td>{a.hasTotp ? 'Aktif' : 'Belum aktif'}</td><td>{a.active ? 'Aktif' : 'Nonaktif'}</td><td>{a.id !== data.admin.id && <button className="btn outline small" onClick={() => setEdit({ ...a, active: !!a.active })}>Ubah akses</button>}</td></tr>)}</tbody></table></div>{data.invitations.length > 0 && <div className="upload-form"><h4>Akun yang diizinkan, belum masuk</h4>{data.invitations.map((i: any) => <p key={i.id}>{i.email} · {roles[i.role]}</p>)}</div>}</div><div className="panel"><div className="panel-head"><h3>Riwayat aktivitas</h3></div><div className="table-scroll"><table><thead><tr><th>Waktu</th><th>Pelaku</th><th>Tindakan & catatan</th></tr></thead><tbody>{data.audit.map((l: any) => <tr key={l.id}><td>{tanggal(l.created_at)}<small>{new Date(l.created_at).toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta' })} WIB</small></td><td>{data.admins.find((a: any) => a.id === l.actor)?.name || l.actor}</td><td>{l.action}<small>{l.detail}</small></td></tr>)}</tbody></table></div></div>{edit && <Dialog title="Ubah hak akses admin" onClose={() => setEdit(null)}><form onSubmit={async (e) => { e.preventDefault(); if (window.confirm('Simpan perubahan hak akses? Sesi akun ini akan diakhiri.') && await perform('admin/admins', edit))
    setEdit(null); }}><p>{edit.email}</p><label>Peran<select value={edit.role} onChange={e => setEdit({ ...edit, role: e.target.value })}>{Object.entries(roles).map(([k, v]) => <option key={k} value={k}>{v}</option>)}</select></label><label className="checkbox"><input type="checkbox" checked={edit.active} onChange={e => setEdit({ ...edit, active: e.target.checked })}/><span>Akun aktif</span></label><button className="btn">Simpan perubahan akses</button></form></Dialog>}</>; }
export default function Admin({ publicData }: {
    publicData: any;
}) {
    const [info, setInfo] = useState<any>(null), [data, setData] = useState<any>(null), [tab, setTab] = useState('summary'), [error, setError] = useState(''), [success, setSuccess] = useState(''), [busy, setBusy] = useState(false), [editing, setEditing] = useState<any>(null), [selected, setSelected] = useState<any>(null), [menu, setMenu] = useState(false);
    async function load() { try {
        const info = await api('auth/info');
        setInfo(info);
        if (info.admin?.factor === 1 && (info.admin.role === 'editor' || info.admin.hasTotp))
            setData(await api('admin/data'));
        else
            setData(null);
    }
    catch (err) {
        setError((err as Error).message);
    } }
    useEffect(() => { load(); }, []);
    async function refresh() { try {
        setData(await api('admin/data'));
    }
    catch (err) {
        setError((err as Error).message);
    } }
    async function perform(path: string, body: any) { if (busy)
        return false; setBusy(true); setError(''); setSuccess(''); try {
        const r = await api(path, body);
        setSuccess(r.note || `Berhasil disimpan.${r.imported !== undefined ? ` ${r.imported} mutasi dimasukkan; ${r.skipped} referensi sudah ada.` : ''}`);
        await refresh();
        return true;
    }
    catch (err) {
        setError((err as Error).message);
        return false;
    }
    finally {
        setBusy(false);
    } }
    async function logout() { await api('auth/logout', {}); setData(null); setInfo(null); window.location.assign('/signout-with-chatgpt?return_to=%2Fadmin'); }
    const create = (type: string) => setEditing({ type, id: null, revision: 0, payload: JSON.parse(JSON.stringify(initialPayload[type])) });
    if (!data)
        return <><AuthGate info={info} refresh={load}/>{error && <div className="toast error" role="alert"><span>{error}</span><button className="icon-button" aria-label="Tutup pesan" onClick={() => setError('')}><X size={17}/></button></div>}</>;
    const permitted = navItems.filter(n => n.roles.includes(data.admin.role));
    const current = permitted.find(n => n.id === tab) || permitted[0];
    const pending = data.donations?.filter((d: any) => d.status === 'waiting_verification') || [];
    let panel: React.ReactNode;
    if (tab === 'summary')
        panel = <><div className="dashboard-intro"><div><span className="eyebrow">Amanah pendidikan Al-Habibah</span><h1>Selamat datang, pengelola.</h1><p>Kelola pembangunan, periksa penerimaan, dan kabarkan setiap perkembangan.</p></div><span className="date-chip"><CalendarDays size={16}/>{tanggal(new Date().toISOString())}</span></div><div className="dashboard-stats"><div><Landmark size={23}/><span>Penerimaan terverifikasi</span><strong>{rupiah(data.finance.receipts)}</strong><small>Bruto dari penerimaan bank</small></div><div><WalletIcon /><span>Saldo dana kampanye</span><strong>{rupiah(data.finance.balance)}</strong><small>Setelah penggunaan, biaya, dan refund</small></div><div><Clock3 size={23}/><span>{data.donations ? 'Menunggu verifikasi' : 'Program tersimpan'}</span><strong>{data.donations ? pending.length : data.records.filter((r: any) => r.type === 'program').length}</strong><small>{data.donations ? 'Periksa bank sebelum memverifikasi' : 'Konten dikelola dari database'}</small></div></div>{data.donations && <div className="panel"><div className="panel-head"><h3>Antrean pemeriksaan pembayaran</h3><button className="text-link" onClick={() => setTab('donations')}>Lihat semua</button></div>{pending.length ? <div className="table-scroll"><table><tbody>{pending.slice(0, 6).map((d: any) => <tr key={d.id}><td><strong>{d.name}</strong><small>{tanggal(d.created_at)}</small></td><td>{rupiah(d.amount)}</td><td><span className="tag waiting_verification">Menunggu verifikasi</span></td><td><button className="btn outline small" onClick={() => { setTab('donations'); setSelected(d); }}>Periksa</button></td></tr>)}</tbody></table></div> : <Empty title="Belum ada donasi menunggu verifikasi" icon={Heart}>Bukti transfer yang dikirim donatur akan masuk ke antrean ini.</Empty>}</div>}<div className="dashboard-two"><div className="panel"><div className="panel-head"><h3>Status layanan</h3></div><div className="service-list"><div><Landmark size={20}/><span>Transfer manual</span><b>{publicData?.paymentReady ? 'Aktif' : 'Rekening perlu dikonfirmasi'}</b></div><div><ShieldCheck size={20}/><span>Authenticator akun</span><b>{data.admin.hasTotp ? 'Aktif' : 'Opsional untuk editor'}</b></div><div><DatabaseBackup size={20}/><span>Backup harian</span><b>{data.backupConfigured && data.backupScheduleActive ? 'Dikonfigurasi' : 'Belum aktif'}</b></div><div><ImageIcon size={20}/><span>Dokumentasi asli</span><b>{data.media.filter((m: any) => m.visibility === 'public' && m.mime.startsWith('image/')).length} foto</b></div></div></div><div className="panel"><div className="panel-head"><h3>Catatan sebelum membuka donasi</h3></div><div className="upload-form"><p>Periksa nomor rekening pada proposal, unggah foto asli, lalu publikasikan kebijakan dan kontak pengelola yang telah dikonfirmasi.</p><p>QRIS, virtual account, dan koneksi bank otomatis belum aktif.</p><a className="btn outline" href="/tentang" target="_blank" rel="noreferrer">Periksa halaman publik<ExternalLink size={16}/></a></div></div></div></>;
    else if (tab === 'programs')
        panel = <RecordList type="program" data={data} edit={setEditing} create={() => create('program')}/>;
    else if (tab === 'progress')
        panel = <RecordList type="progress" data={data} edit={setEditing} create={() => create('progress')}/>;
    else if (tab === 'content') {
        const site = data.records.find((r: any) => r.type === 'site');
        panel = <><div className="panel"><div className="panel-head"><h3>Hero, profil, kontak & identitas situs</h3><button className="btn small" onClick={() => setEditing(site)}>Ubah konten situs</button></div><div className="upload-form"><span className="eyebrow">Versi {site.revision} · {publication[site.state]}</span><h2>{site.payload.title}</h2><p>{site.payload.description}</p><p className="muted">Tombol, logo, foto, urutan bagian, dokumen, profil, footer, dan SEO dapat diperbarui di sini.</p></div></div><RecordList type="faq" data={data} edit={setEditing} create={() => create('faq')}/></>;
    }
    else if (tab === 'gallery')
        panel = <GalleryPanel data={data} refresh={refresh}/>;
    else if (tab === 'donations')
        panel = <DonationsPanel data={data} perform={perform} selected={selected} setSelected={setSelected}/>;
    else if (tab === 'reports')
        panel = <FinancePanel data={data} perform={perform} edit={setEditing} create={() => create('report')}/>;
    else if (tab === 'reconcile')
        panel = <ReconcilePanel data={data} perform={perform}/>;
    else if (tab === 'versions')
        panel = <VersionsPanel data={data} perform={perform} edit={setEditing}/>;
    else if (tab === 'security')
        panel = <SecurityPanel data={data} refresh={refresh}/>;
    else if (tab === 'settings')
        panel = <SettingsPanel data={data} perform={perform}/>;
    else if (tab === 'backups')
        panel = <BackupPanel data={data} perform={perform}/>;
    else if (tab === 'admins')
        panel = <AccountsPanel data={data} perform={perform}/>;
    return <div className="admin-shell"><aside className={'admin-sidebar ' + (menu ? 'visible' : '')}><a href="/admin" className="admin-brand"><span className="brand-mark"><BookOpen size={23}/></span><span>Al-Habibah<small>Ruang pengelola</small></span></a><span className="sidebar-label">PENGELOLAAN</span><nav aria-label="Menu dashboard">{permitted.map(n => <button key={n.id} className={tab === n.id ? 'active' : ''} onClick={() => { setTab(n.id); setMenu(false); setError(''); setSuccess(''); }}><n.icon size={19}/>{n.name}{n.id === 'donations' && pending.length > 0 && <b>{pending.length}</b>}</button>)}</nav><div className="sidebar-foot"><a href="/" target="_blank" rel="noreferrer"><ExternalLink size={18}/>Lihat situs donasi</a><button onClick={logout}><LogOut size={18}/>Keluar</button><span>{roles[data.admin.role]} · {data.admin.email}</span></div></aside><div className="admin-main"><header className="admin-topbar"><div><button className="menu-button" onClick={() => setMenu(!menu)} aria-label="Buka menu dashboard"><LayoutDashboard size={21}/></button><span>Dashboard <span className="muted">/</span> <strong>{current.name}</strong></span></div><div className="admin-top-actions">{busy && <span className="saving"><Loader2 className="spin" size={16}/>Menyimpan…</span>}<button className="icon-button" aria-label="Perbarui data" onClick={refresh}><RefreshCw size={18}/></button><span className="role-chip">{roles[data.admin.role]}</span><span className="avatar">{data.admin.name.slice(0, 1).toUpperCase()}</span></div></header><main className="admin-workspace">{tab !== 'summary' && <div className="workspace-title"><span className="eyebrow">Pengelolaan Al-Habibah</span><h1>{current.name}</h1></div>}{panel}</main></div>{(error || success) && <div className={'toast ' + (error ? 'error' : 'success')} role={error ? 'alert' : 'status'}>{error ? <LockKeyhole size={19}/> : <Check size={19}/>}<span>{error || success}</span><button className="icon-button" aria-label="Tutup pesan" onClick={() => { setError(''); setSuccess(''); }}><X size={17}/></button></div>}{editing && <RecordEditor record={editing} data={data} onClose={() => setEditing(null)} onSaved={() => { setSuccess('Konten tersimpan. Versi baru dicatat.'); refresh(); }}/>}</div>;
}
function WalletIcon() { return <Landmark size={23}/>; }
