// 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 ? (
) : 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 (
); })}
Judul { setTitle(e.target.value); setErr(''); }} placeholder={kind === 'temuan' ? 'cth. Menemukan kucing oren di Allevare' : 'cth. Kehilangan dompet kulit coklat'} />
Deskripsi (opsional)
Lokasi terakhir setLoc(e.target.value)} placeholder="cth. Sekitar Pasar Modern BSD" />
Cluster (opsional)
Kontak WhatsApp setWa(e.target.value)} placeholder="08xxxxxxxxxx" type="tel" />
{err &&
{err}
}
);
}
function LostFoundEditFlow({ item, clusters = [], onClose, onDone }) {
const [kind, setKind] = React.useState(item.kind || 'hilang');
const [cat, setCat] = React.useState(item.category || 'barang');
const [title, setTitle] = React.useState(item.title || '');
const [body, setBody] = React.useState(item.body || '');
const [loc, setLoc] = React.useState(item.location_text || '');
const [clusterId, setClusterId] = React.useState(item.cluster_id || '');
const [wa, setWa] = React.useState(item.contact_wa || '');
const [photoFile, setPhotoFile] = React.useState(null);
const [photoPrev, setPhotoPrev] = React.useState(item.photo_url || null);
const [err, setErr] = React.useState('');
const [busy, setBusy] = 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); setPhotoPrev(URL.createObjectURL(f)); };
const submit = async () => {
if (!title.trim()) return setErr('Judul wajib diisi');
setBusy(true); setErr('');
const res = await gsData.updateLostFound(item.id, { 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);
onDone();
};
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 ? 'Menyimpan...' : 'Simpan perubahan'}{!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 (
); })}
Judul { setTitle(e.target.value); setErr(''); }} />
Deskripsi
Lokasi terakhir setLoc(e.target.value)} />
Cluster (opsional)
Kontak WhatsApp setWa(e.target.value)} placeholder="08xxxxxxxxxx" type="tel" />
{err &&
{err}
}
);
}
Object.assign(window, { LostFoundScreen, LFDetailSheet, LostFoundFlow, LostFoundEditFlow, LF_CATS });