import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ArrowLeft, ArrowRight, ArrowUp, ArrowDown, ArrowUpDown, ChevronDown, Columns2, Database, Eye, Pencil, RefreshCw, Search, Trash2, X, AlertTriangle, Inbox, } from 'lucide-react' import { toast } from '../store/useToastStore' import { apiRequest, hasApi } from '../utils/api' import { fieldLabel, flattenInputFields, formatEntryValue } from '../utils/entries' import { applyFilters, Slot, useRegistryVersion } from '../extensions/registry' const PAGE_SIZES = [10, 25, 50] const SELECT_CLS = '!px-3.5 !pr-8 !py-2 !text-sm !font-semibold !border !border-slate-300 !rounded-xl !bg-white !text-slate-800 !shadow-sm !min-h-[38px] !leading-5 !max-w-none focus:!ring-2 focus:!ring-slate-900 focus:!outline-none cursor-pointer' const BTN_SECONDARY = 'inline-flex items-center gap-1.5 px-3.5 py-2 text-sm font-semibold bg-white border border-slate-300 text-slate-800 rounded-xl hover:bg-slate-50 transition-colors shadow-sm cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed' const parseDate = (s) => new Date(String(s || '').replace(' ', 'T')) /** * Entries of one form. Extensions add toolbar buttons (slot `entries.toolbar`, receives { entries, fields, formId, * formName }), render stored values (`entries.renderValue` filter: (null, { field, value, context }) → element) and * mark fields read-only in the edit dialog (`entries.editable` filter). */ export default function EntriesScreen({ forms, formsLoading, initialFormId, onBack, onEditForm, onFormChange }) { useRegistryVersion() const [formId, setFormId] = useState(initialFormId || '') const [formName, setFormName] = useState('') const [fields, setFields] = useState([]) const [entries, setEntries] = useState([]) const [loading, setLoading] = useState(false) const [search, setSearch] = useState('') const [sort, setSort] = useState({ key: 'created_at', dir: 'desc' }) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [selected, setSelected] = useState([]) const [bulkAction, setBulkAction] = useState('') const [visibleColumns, setVisibleColumns] = useState({}) const [showColSelector, setShowColSelector] = useState(false) const [selectedEntry, setSelectedEntry] = useState(null) const [isEntryDetailOpen, setIsEntryDetailOpen] = useState(false) const [isEntryEditOpen, setIsEntryEditOpen] = useState(false) const [editEntryData, setEditEntryData] = useState({}) const [deleteTarget, setDeleteTarget] = useState(null) // entry | 'bulk' const [deleting, setDeleting] = useState(false) useEffect(() => { if (initialFormId) setFormId(initialFormId) }, [initialFormId]) // No form chosen yet: open the most recent one, like the reference builder does. useEffect(() => { if (!formId && forms && forms.length) { setFormId(forms[0].id) if (onFormChange) onFormChange(forms[0].id, forms[0].name, true) } }, [formId, forms]) // eslint-disable-line react-hooks/exhaustive-deps const loadEntries = useCallback(async (id) => { if (!id || !hasApi()) return setLoading(true) try { const [detail, list] = await Promise.all([ apiRequest(`/forms/${id}`).catch(() => null), apiRequest(`/forms/${id}/entries`), ]) const listed = (forms || []).find((f) => f.id === id) let rawFields = detail ? detail.fields : listed && listed.fields if (typeof rawFields === 'string') { try { rawFields = JSON.parse(rawFields) } catch (e) { rawFields = [] } } setFields(Array.isArray(rawFields) ? rawFields : []) setFormName((detail && detail.name) || (listed && listed.name) || '') setEntries(Array.isArray(list) ? list : []) } catch (err) { toast.error(`Could not load entries: ${err.message}`) setEntries([]) } finally { setLoading(false) } }, [forms]) useEffect(() => { setSelected([]); setPage(1); setSearch(''); setBulkAction('') loadEntries(formId) }, [formId]) // eslint-disable-line react-hooks/exhaustive-deps const inputFields = useMemo(() => flattenInputFields(fields), [fields]) const displayFields = inputFields.filter((f) => visibleColumns[f.id] !== false) const filtered = useMemo(() => { const q = search.trim().toLowerCase() if (!q) return entries return entries.filter((e) => { const data = e.data || {} if (Object.values(data).some((v) => formatEntryValue(v).toLowerCase().includes(q))) return true return String(e.user_ip || '').toLowerCase().includes(q) }) }, [entries, search]) const sorted = useMemo(() => { const val = (e) => (sort.key === 'created_at' ? String(e.created_at || '') : formatEntryValue(e.data && e.data[sort.key]).toLowerCase()) return filtered .map((e, i) => ({ e, i })) .sort((a, b) => { const av = val(a.e), bv = val(b.e) // Same timestamp: keep the server order (newest first), reversed for ascending. if (av === bv) return sort.dir === 'asc' ? b.i - a.i : a.i - b.i return (av > bv ? 1 : -1) * (sort.dir === 'asc' ? 1 : -1) }) .map((x) => x.e) }, [filtered, sort]) const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize)) useEffect(() => { if (page > totalPages) setPage(totalPages) }, [page, totalPages]) const pageRows = sorted.slice((page - 1) * pageSize, page * pageSize) useEffect(() => { const ids = new Set(filtered.map((e) => e.id)) setSelected((s) => (s.every((id) => ids.has(id)) ? s : s.filter((id) => ids.has(id)))) }, [filtered]) const allOnPageSelected = pageRows.length > 0 && pageRows.every((e) => selected.includes(e.id)) const someOnPageSelected = pageRows.some((e) => selected.includes(e.id)) && !allOnPageSelected const selectAllRef = useRef(null) useEffect(() => { if (selectAllRef.current) selectAllRef.current.indeterminate = someOnPageSelected }, [someOnPageSelected]) const onSort = (key) => setSort((s) => (s.key === key ? { key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: key === 'created_at' ? 'desc' : 'asc' })) const handleFormSelect = (id) => { setFormId(id) const f = (forms || []).find((x) => x.id === id) if (onFormChange) onFormChange(id, f ? f.name : '') } const handleConfirmDelete = async () => { setDeleting(true) try { if (deleteTarget === 'bulk') { const res = await apiRequest('/entries/bulk-delete', { method: 'POST', body: { ids: selected } }) toast.success(`Deleted ${res.deleted} ${res.deleted === 1 ? 'entry' : 'entries'}`) setSelected([]) setBulkAction('') } else { await apiRequest(`/entries/${deleteTarget.id}`, { method: 'DELETE' }) toast.success('Entry deleted successfully!') } setDeleteTarget(null) await loadEntries(formId) } catch (err) { toast.error(`Failed to delete: ${err.message}`) } finally { setDeleting(false) } } const handleSaveEntry = async () => { if (!selectedEntry) return try { const data = await apiRequest(`/entries/${selectedEntry.id}`, { method: 'POST', body: { data: editEntryData } }) if (data && data.success) { toast.success('Entry updated successfully!') setIsEntryEditOpen(false) loadEntries(formId) } else { toast.error('Failed to update entry.') } } catch (err) { toast.error(`Failed to update entry: ${err.message}`) } } const currentForm = (forms || []).find((f) => f.id === formId) const hasForms = forms && forms.length > 0 return (
{/* Header */}

Entries

{formId && !loading && ( {entries.length} submissions )}

View, filter and manage your form submission data.

{/* Form selector */}
{formId && ( )}
Showing entries for {formName || (currentForm && currentForm.name) || '—'}
{/* Actions bar */}
{showColSelector && ( <>
setShowColSelector(false)} />
Visible Fields
{inputFields.map((field) => ( ))}
)}
{/* Table */}
{!hasForms && !formsLoading ? (

No forms available

Create a form first to start collecting entries.

) : (
{displayFields.map((field) => ( ))} {loading || (formsLoading && !formId) ? ( Array.from({ length: 5 }).map((_, i) => ( )) ) : pageRows.length === 0 ? ( ) : pageRows.map((entry) => { const isSel = selected.includes(entry.id) return ( {displayFields.map((field) => { const raw = entry.data ? entry.data[field.id] : '' return ( ) })} ) })}
{ const ids = pageRows.map((x) => x.id) setSelected((s) => (e.target.checked ? Array.from(new Set([...s, ...ids])) : s.filter((id) => !ids.includes(id)))) }} className="cursor-pointer" /> IP Address Actions
{search ? : }

{search ? 'No matching entries' : 'No submissions yet'}

{search ? 'No entries match your search.' : 'Embed this form on your website to start collecting data.'}

setSelected((s) => (e.target.checked ? [...s, entry.id] : s.filter((x) => x !== entry.id)))} className="cursor-pointer" /> {applyFilters('entries.renderValue', null, { field, value: raw, context: 'cell' }) || (formatEntryValue(raw) || '—')} {entry.user_ip || '—'} {parseDate(entry.created_at).toLocaleString()}
)}
{/* Pagination */} {formId && (
{sorted.length} {sorted.length === 1 ? 'entry' : 'entries'}
{[['First', 1], ['Prev', page - 1]].map(([label, to]) => ( ))} {page} / {totalPages} {[['Next', page + 1], ['Last', totalPages]].map(([label, to]) => ( ))}
)}
{deleteTarget && (

{deleteTarget === 'bulk' ? `Delete ${selected.length} ${selected.length === 1 ? 'entry' : 'entries'}?` : 'Delete entry?'}

This submission data will be permanently deleted. This cannot be undone.

)} {isEntryDetailOpen && selectedEntry && (

Submission Details

Submitted at {parseDate(selectedEntry.created_at).toLocaleString()} from IP {selectedEntry.user_ip}

{inputFields.map((field) => { const value = selectedEntry.data ? selectedEntry.data[field.id] : undefined return (
{applyFilters('entries.renderValue', null, { field, value, context: 'detail' }) || (typeof value === 'object' && value !== null ? (
{Object.entries(value).map(([k, v]) => (
{k}: {formatEntryValue(v)}
))}
) : ( {String(value || '-')} ))}
) })}
)} {isEntryEditOpen && selectedEntry && (

Edit Submission

Modify the customer response values.

{inputFields.filter((f) => applyFilters('entries.editable', true, f) !== false).map((field) => { const value = editEntryData[field.id] || '' return (
{field.type === 'textarea' ? (