// lostfound.jsx — Kehilangan & Temuan. Browse + detail + lapor. Global. const LF_CATS = [ { key: 'barang', label: 'Barang', icon: 'tag', color: '#7C3AED' }, { key: 'hewan', label: 'Hewan', icon: 'paw', color: '#F59E0B' }, { key: 'dokumen', label: 'Dokumen', icon: 'doc', color: '#1B4FD8' }, { key: 'kendaraan', label: 'Kendaraan', icon: 'car', color: '#14B8A6' }, { key: 'lainnya', label: 'Lainnya', icon: 'grid', color: '#64748B' }, ]; const LF_CAT = LF_CATS.reduce((a, c) => (a[c.key] = c, a), {}); function lfKindMeta(kind) { return kind === 'temuan' ? { label: 'TEMUAN', bg: 'var(--lime-50)', fg: 'var(--lime-700)', dot: 'var(--lime)' } : { label: 'HILANG', bg: 'var(--coral-50)', fg: 'var(--coral-700)', dot: 'var(--coral)' }; } function lfTime(iso) { const d = new Date(iso), now = new Date(), diff = (now - d) / 1000; if (diff < 3600) return Math.max(1, Math.floor(diff / 60)) + ' mnt lalu'; if (diff < 86400) return Math.floor(diff / 3600) + ' jam lalu'; if (diff < 604800) return Math.floor(diff / 86400) + ' hari lalu'; return d.getDate() + ' ' + ID_MON[d.getMonth()]; } function LFCard({ item, onOpen }) { const cc = LF_CAT[item.category] || LF_CAT.lainnya; const km = lfKindMeta(item.kind); const resolved = item.status === 'resolved'; return ( ); } function LostFoundScreen({ authed, onRequireAuth, profile, clusters = [], onCreate }) { const [items, setItems] = React.useState([]); const [kind, setKind] = React.useState('all'); const [cat, setCat] = React.useState('all'); const [loading, setLoading] = React.useState(true); const [detail, setDetail] = React.useState(null); const load = React.useCallback(() => { setLoading(true); gsData.loadLostFound({ kind: kind, category: cat }).then(d => { setItems(d); setLoading(false); }); }, [kind, cat]); React.useEffect(() => { load(); }, [load]); const isOwner = detail && profile && detail.user_id === profile.id; return (

Kehilangan & Temuan

{[['all', 'Semua'], ['hilang', 'Hilang'], ['temuan', 'Temuan']].map(([k, l]) => ( ))}
setCat('all')}>Semua jenis {LF_CATS.map(c => setCat(c.key)}>{c.label})}
{loading ? (
{[0, 1, 2].map(i =>
)}
) : items.length === 0 ? (
Belum ada laporan
Kehilangan sesuatu? Atau menemukan barang? Laporkan di sini.
) : (
{items.map(it => )}
)}
{detail && setDetail(null)} onChanged={() => { setDetail(null); load(); }} />}
); } function LFDetailSheet({ item, isOwner, authed, clusters = [], onRequireAuth, onClose, onChanged }) { const cc = LF_CAT[item.category] || LF_CAT.lainnya; const km = lfKindMeta(item.kind); const [busy, setBusy] = React.useState(false); const [editing, setEditing] = React.useState(false); const [confirmDel, setConfirmDel] = React.useState(false); if (editing) return
setEditing(false)} onDone={() => { setEditing(false); onChanged(); }} />
; const wa = item.contact_wa ? waLink(item.contact_wa, 'Halo, soal ' + (item.kind === 'temuan' ? 'barang temuan' : 'kehilangan') + ' "' + item.title + '" di gs.town.') : null; const mapUrl = item.lat != null && item.lng != null ? 'https://www.google.com/maps/search/?api=1&query=' + item.lat + ',' + item.lng : (item.location_text ? 'https://www.google.com/maps/search/?api=1&query=' + encodeURIComponent(item.location_text + ' Gading Serpong') : null); const act = async (fn) => { setBusy(true); await fn(); setBusy(false); onChanged(); }; return (
{item.photo_url ? : } {km.label}
{cc.label} {item.status === 'resolved' && · SELESAI}

{item.title}

{item.body &&

{item.body}

}
{item.location_text && ( )}
{item.reporter}
Pelapor · {lfTime(item.created_at)}{item.cluster ? ' · ' + item.cluster : ''}
{isOwner ? (
{item.status !== 'resolved' && }
{confirmDel && setConfirmDel(false)} onConfirm={() => { setConfirmDel(false); act(() => gsData.deleteLostFound(item.id)); }} />}
) : !item.contact_wa ?
Pelapor tidak mencantumkan kontak.
: !authed ? ( ) : ( Hubungi pelapor )}
); } function LostFoundFlow({ clusters, profile, onClose, onDone }) { const [kind, setKind] = React.useState('hilang'); const [cat, setCat] = React.useState('barang'); const [title, setTitle] = React.useState(''); const [body, setBody] = React.useState(''); const [loc, setLoc] = React.useState(''); const [clusterId, setClusterId] = React.useState(''); const [wa, setWa] = React.useState(''); const [photoFile, setPhotoFile] = React.useState(null); const [photoPrev, setPhotoPrev] = React.useState(null); const [err, setErr] = React.useState(''); const [busy, setBusy] = React.useState(false); const [done, setDone] = React.useState(false); const fileRef = React.useRef(null); const onFile = (e) => { const f = e.target.files && e.target.files[0]; if (!f) return; setPhotoFile(f); if (photoPrev) URL.revokeObjectURL(photoPrev); setPhotoPrev(URL.createObjectURL(f)); }; const submit = async () => { if (!title.trim()) return setErr('Judul wajib diisi'); setBusy(true); setErr(''); const res = await gsData.submitLostFound({ kind, category: cat, title, body, location_text: loc, cluster_id: clusterId || null, contact_wa: wa, photoFile }); setBusy(false); if (res.error) return setErr('Gagal: ' + res.error); setDone(true); setTimeout(onDone, 1400); }; if (done) return (

Laporan terkirim!

Semoga cepat ketemu. Warga lain bisa membantu sekarang.

); const fld = { width: '100%', padding: '13px 14px', borderRadius: 'var(--r-md)', border: '1px solid var(--border)', background: 'var(--surface-2)', fontFamily: 'var(--font)', fontSize: 14.5, fontWeight: 500, color: 'var(--text-1)', outline: 'none' }; return ( {busy ? 'Mengirim...' : 'Kirim Laporan'}{!busy && }}>
Jenis laporan
{[['hilang', 'Kehilangan', 'var(--coral)'], ['temuan', 'Menemukan', 'var(--lime)']].map(([k, l, col]) => ( ))}
Jenis barang
{LF_CATS.map(c => { const on = cat === c.key; return ( ); })}
Foto (sangat membantu)
Judul { setTitle(e.target.value); setErr(''); }} placeholder={kind === 'temuan' ? 'cth. Menemukan kucing oren di Allevare' : 'cth. Kehilangan dompet kulit coklat'} />
Deskripsi (opsional)