@actuate-media/cms-admin 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/__tests__/components/forms-pagination.render.test.d.ts +2 -0
  2. package/dist/__tests__/components/forms-pagination.render.test.d.ts.map +1 -0
  3. package/dist/__tests__/components/forms-pagination.render.test.js +33 -0
  4. package/dist/__tests__/components/forms-pagination.render.test.js.map +1 -0
  5. package/dist/__tests__/layout/sidebar-forms-badge.render.test.d.ts +2 -0
  6. package/dist/__tests__/layout/sidebar-forms-badge.render.test.d.ts.map +1 -0
  7. package/dist/__tests__/layout/sidebar-forms-badge.render.test.js +30 -0
  8. package/dist/__tests__/layout/sidebar-forms-badge.render.test.js.map +1 -0
  9. package/dist/__tests__/layout/sidebar-submenu.render.test.js +8 -2
  10. package/dist/__tests__/layout/sidebar-submenu.render.test.js.map +1 -1
  11. package/dist/__tests__/views/form-submissions.render.test.d.ts +2 -0
  12. package/dist/__tests__/views/form-submissions.render.test.d.ts.map +1 -0
  13. package/dist/__tests__/views/form-submissions.render.test.js +110 -0
  14. package/dist/__tests__/views/form-submissions.render.test.js.map +1 -0
  15. package/dist/__tests__/views/forms-list.render.test.d.ts +2 -0
  16. package/dist/__tests__/views/forms-list.render.test.d.ts.map +1 -0
  17. package/dist/__tests__/views/forms-list.render.test.js +122 -0
  18. package/dist/__tests__/views/forms-list.render.test.js.map +1 -0
  19. package/dist/actuate-admin.css +1 -1
  20. package/dist/components/forms/Drawer.d.ts +13 -0
  21. package/dist/components/forms/Drawer.d.ts.map +1 -0
  22. package/dist/components/forms/Drawer.js +13 -0
  23. package/dist/components/forms/Drawer.js.map +1 -0
  24. package/dist/components/forms/EmbedPanel.d.ts +7 -0
  25. package/dist/components/forms/EmbedPanel.d.ts.map +1 -0
  26. package/dist/components/forms/EmbedPanel.js +91 -0
  27. package/dist/components/forms/EmbedPanel.js.map +1 -0
  28. package/dist/components/forms/EntryDetailDrawer.d.ts +11 -0
  29. package/dist/components/forms/EntryDetailDrawer.d.ts.map +1 -0
  30. package/dist/components/forms/EntryDetailDrawer.js +145 -0
  31. package/dist/components/forms/EntryDetailDrawer.js.map +1 -0
  32. package/dist/components/forms/FieldsPanel.d.ts +8 -0
  33. package/dist/components/forms/FieldsPanel.d.ts.map +1 -0
  34. package/dist/components/forms/FieldsPanel.js +123 -0
  35. package/dist/components/forms/FieldsPanel.js.map +1 -0
  36. package/dist/components/forms/FormSchemaDrawer.d.ts +9 -0
  37. package/dist/components/forms/FormSchemaDrawer.d.ts.map +1 -0
  38. package/dist/components/forms/FormSchemaDrawer.js +96 -0
  39. package/dist/components/forms/FormSchemaDrawer.js.map +1 -0
  40. package/dist/components/forms/NotificationsPanel.d.ts +6 -0
  41. package/dist/components/forms/NotificationsPanel.d.ts.map +1 -0
  42. package/dist/components/forms/NotificationsPanel.js +80 -0
  43. package/dist/components/forms/NotificationsPanel.js.map +1 -0
  44. package/dist/components/forms/primitives.d.ts +57 -0
  45. package/dist/components/forms/primitives.d.ts.map +1 -0
  46. package/dist/components/forms/primitives.js +131 -0
  47. package/dist/components/forms/primitives.js.map +1 -0
  48. package/dist/layout/Sidebar.d.ts +5 -0
  49. package/dist/layout/Sidebar.d.ts.map +1 -1
  50. package/dist/layout/Sidebar.js +43 -3
  51. package/dist/layout/Sidebar.js.map +1 -1
  52. package/dist/lib/forms-events.d.ts +17 -0
  53. package/dist/lib/forms-events.d.ts.map +1 -0
  54. package/dist/lib/forms-events.js +20 -0
  55. package/dist/lib/forms-events.js.map +1 -0
  56. package/dist/lib/forms-service.d.ts +114 -0
  57. package/dist/lib/forms-service.d.ts.map +1 -0
  58. package/dist/lib/forms-service.js +210 -0
  59. package/dist/lib/forms-service.js.map +1 -0
  60. package/dist/views/FormSubmissions.d.ts.map +1 -1
  61. package/dist/views/FormSubmissions.js +224 -77
  62. package/dist/views/FormSubmissions.js.map +1 -1
  63. package/dist/views/Forms.d.ts.map +1 -1
  64. package/dist/views/Forms.js +135 -17
  65. package/dist/views/Forms.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/__tests__/components/forms-pagination.render.test.tsx +49 -0
  68. package/src/__tests__/layout/sidebar-forms-badge.render.test.tsx +50 -0
  69. package/src/__tests__/layout/sidebar-submenu.render.test.tsx +9 -2
  70. package/src/__tests__/views/form-submissions.render.test.tsx +129 -0
  71. package/src/__tests__/views/forms-list.render.test.tsx +141 -0
  72. package/src/components/forms/Drawer.tsx +70 -0
  73. package/src/components/forms/EmbedPanel.tsx +173 -0
  74. package/src/components/forms/EntryDetailDrawer.tsx +312 -0
  75. package/src/components/forms/FieldsPanel.tsx +385 -0
  76. package/src/components/forms/FormSchemaDrawer.tsx +185 -0
  77. package/src/components/forms/NotificationsPanel.tsx +240 -0
  78. package/src/components/forms/primitives.tsx +300 -0
  79. package/src/layout/Sidebar.tsx +72 -6
  80. package/src/lib/forms-events.ts +32 -0
  81. package/src/lib/forms-service.ts +343 -0
  82. package/src/views/FormSubmissions.tsx +529 -394
  83. package/src/views/Forms.tsx +381 -106
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Forms service — typed client over the cms-core Forms REST endpoints.
3
+ *
4
+ * Every Forms admin component reads/writes through this module (never raw
5
+ * `cmsApi` / `fetch`), so the network contract lives in one place. Read
6
+ * fetchers re-surface `cmsApi`'s `{ error }` as a thrown error (so the
7
+ * resource hook can show error/retry UI); mutations return `{ error? }`.
8
+ *
9
+ * The endpoint paths here are the contract implemented by cms-core
10
+ * `packages/cms-core/src/api/handlers.ts` (PR 2). Domain types are re-used
11
+ * from `@actuate-media/cms-core` so the admin can never drift from the server.
12
+ */
13
+ import { cmsApi } from './api.js';
14
+ /**
15
+ * `cmsApi` never throws — it returns `{ error, status }` on a network/HTTP
16
+ * failure. Read (GET) fetchers must re-surface that as a thrown error so the
17
+ * resource hook can distinguish "request failed" from "empty result".
18
+ */
19
+ function throwIfError(res) {
20
+ if (res.error)
21
+ throw new Error(res.error);
22
+ }
23
+ // ─── Forms list / stats / sidebar ────────────────────────────────────────
24
+ function buildFormsQuery(params) {
25
+ const qs = new URLSearchParams();
26
+ if (params.search)
27
+ qs.set('search', params.search);
28
+ if (params.status)
29
+ qs.set('status', params.status);
30
+ if (typeof params.notifyEnabled === 'boolean') {
31
+ qs.set('notifyEnabled', String(params.notifyEnabled));
32
+ }
33
+ if (params.sortBy)
34
+ qs.set('sortBy', params.sortBy);
35
+ if (params.sortDirection)
36
+ qs.set('sortDirection', params.sortDirection);
37
+ if (params.page)
38
+ qs.set('page', String(params.page));
39
+ if (params.pageSize)
40
+ qs.set('pageSize', String(params.pageSize));
41
+ const str = qs.toString();
42
+ return str ? `?${str}` : '';
43
+ }
44
+ export async function fetchForms(params = {}) {
45
+ const res = await cmsApi(`/forms${buildFormsQuery(params)}`);
46
+ throwIfError(res);
47
+ return res.data ?? [];
48
+ }
49
+ export async function fetchFormById(id) {
50
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}`);
51
+ throwIfError(res);
52
+ return res.data ?? null;
53
+ }
54
+ const EMPTY_STATS = {
55
+ totalForms: 0,
56
+ activeForms: 0,
57
+ totalEntries: 0,
58
+ unreadEntries: 0,
59
+ };
60
+ export async function fetchFormStats() {
61
+ const res = await cmsApi('/forms/stats');
62
+ throwIfError(res);
63
+ return res.data ?? EMPTY_STATS;
64
+ }
65
+ export async function fetchFormsSidebarCounts() {
66
+ const res = await cmsApi('/forms/sidebar-counts');
67
+ throwIfError(res);
68
+ return res.data ?? [];
69
+ }
70
+ export async function createForm(payload) {
71
+ const res = await cmsApi('/forms', {
72
+ method: 'POST',
73
+ body: JSON.stringify(payload),
74
+ });
75
+ return { data: res.data, error: res.error };
76
+ }
77
+ export async function updateForm(id, payload) {
78
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}`, {
79
+ method: 'PATCH',
80
+ body: JSON.stringify(payload),
81
+ });
82
+ return { data: res.data, error: res.error };
83
+ }
84
+ export async function duplicateForm(id) {
85
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}/duplicate`, {
86
+ method: 'POST',
87
+ });
88
+ return { data: res.data, error: res.error };
89
+ }
90
+ /** Move a form to the archived state (hidden from embed pickers, no new submissions). */
91
+ export async function archiveForm(id) {
92
+ return updateForm(id, { status: 'archived' });
93
+ }
94
+ /** Restore an archived form back to draft. */
95
+ export async function restoreForm(id) {
96
+ return updateForm(id, { status: 'draft' });
97
+ }
98
+ export async function deleteForm(id, opts = {}) {
99
+ const qs = opts.force ? '?force=true' : '';
100
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}${qs}`, { method: 'DELETE' });
101
+ return { error: res.error };
102
+ }
103
+ // ─── Schema ──────────────────────────────────────────────────────────────
104
+ export async function fetchFormSchema(id) {
105
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}/schema`);
106
+ throwIfError(res);
107
+ return res.data ?? { fields: [], activeVersionId: null, versions: [] };
108
+ }
109
+ export async function saveFormSchema(id, fields, notes) {
110
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}/schema`, { method: 'PUT', body: JSON.stringify({ fields, notes }) });
111
+ return { data: res.data, error: res.error };
112
+ }
113
+ // ─── Notifications ─────────────────────────────────────────────────────────
114
+ export async function fetchNotificationSettings(id) {
115
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}/notifications`);
116
+ throwIfError(res);
117
+ return res.data ?? { enabled: false, recipients: [] };
118
+ }
119
+ export async function updateNotificationSettings(id, payload) {
120
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}/notifications`, { method: 'PUT', body: JSON.stringify(payload) });
121
+ return { data: res.data, error: res.error };
122
+ }
123
+ /** Lightweight notify on/off toggle used by the forms-list switch. */
124
+ export async function setNotifyEnabled(id, enabled) {
125
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}/notify`, {
126
+ method: 'PUT',
127
+ body: JSON.stringify({ enabled }),
128
+ });
129
+ return { data: res.data, error: res.error };
130
+ }
131
+ // ─── Embed ───────────────────────────────────────────────────────────────
132
+ export async function fetchEmbedSettings(id) {
133
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}/embed`);
134
+ throwIfError(res);
135
+ return res.data ?? {};
136
+ }
137
+ export async function updateEmbedSettings(id, payload) {
138
+ const res = await cmsApi(`/forms/${encodeURIComponent(id)}/embed`, {
139
+ method: 'PUT',
140
+ body: JSON.stringify(payload),
141
+ });
142
+ return { data: res.data, error: res.error };
143
+ }
144
+ function buildEntriesQuery(params) {
145
+ const qs = new URLSearchParams();
146
+ if (params.search)
147
+ qs.set('search', params.search);
148
+ if (params.formId)
149
+ qs.set('formId', params.formId);
150
+ if (typeof params.unread === 'boolean')
151
+ qs.set('unread', String(params.unread));
152
+ if (typeof params.starred === 'boolean')
153
+ qs.set('starred', String(params.starred));
154
+ if (typeof params.archived === 'boolean')
155
+ qs.set('archived', String(params.archived));
156
+ if (params.spamStatus)
157
+ qs.set('spamStatus', params.spamStatus);
158
+ if (params.dateFrom)
159
+ qs.set('dateFrom', params.dateFrom);
160
+ if (params.dateTo)
161
+ qs.set('dateTo', params.dateTo);
162
+ if (params.thisWeek)
163
+ qs.set('thisWeek', 'true');
164
+ if (params.sortBy)
165
+ qs.set('sortBy', params.sortBy);
166
+ if (params.sortDirection)
167
+ qs.set('sortDirection', params.sortDirection);
168
+ if (params.page)
169
+ qs.set('page', String(params.page));
170
+ if (params.pageSize)
171
+ qs.set('pageSize', String(params.pageSize));
172
+ const str = qs.toString();
173
+ return str ? `?${str}` : '';
174
+ }
175
+ const EMPTY_PAGE = { entries: [], total: 0, page: 1, pageSize: 25 };
176
+ export async function fetchEntries(params = {}) {
177
+ const res = await cmsApi(`/forms/entries${buildEntriesQuery(params)}`);
178
+ throwIfError(res);
179
+ return res.data ?? EMPTY_PAGE;
180
+ }
181
+ const EMPTY_COUNTS = { total: 0, unread: 0, starred: 0, thisWeek: 0, byForm: {} };
182
+ export async function fetchEntryCounts(params = {}) {
183
+ const res = await cmsApi(`/forms/entries/counts${buildEntriesQuery(params)}`);
184
+ throwIfError(res);
185
+ return res.data ?? EMPTY_COUNTS;
186
+ }
187
+ export async function fetchEntry(id) {
188
+ const res = await cmsApi(`/forms/entries/${encodeURIComponent(id)}`);
189
+ throwIfError(res);
190
+ return res.data ?? null;
191
+ }
192
+ export async function updateEntry(id, patch) {
193
+ const res = await cmsApi(`/forms/entries/${encodeURIComponent(id)}`, {
194
+ method: 'PATCH',
195
+ body: JSON.stringify(patch),
196
+ });
197
+ return { data: res.data, error: res.error };
198
+ }
199
+ export async function deleteEntry(id) {
200
+ const res = await cmsApi(`/forms/entries/${encodeURIComponent(id)}`, { method: 'DELETE' });
201
+ return { error: res.error };
202
+ }
203
+ export async function bulkUpdateEntries(ids, patch) {
204
+ const res = await cmsApi('/forms/entries/bulk', {
205
+ method: 'POST',
206
+ body: JSON.stringify({ ids, patch }),
207
+ });
208
+ return { data: res.data, error: res.error };
209
+ }
210
+ //# sourceMappingURL=forms-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms-service.js","sourceRoot":"","sources":["../../src/lib/forms-service.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"FormSubmissions.d.ts","sourceRoot":"","sources":["../../src/views/FormSubmissions.tsx"],"names":[],"mappings":"AA0EA,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAED,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,oBAAoB,2CA4X3E"}
1
+ {"version":3,"file":"FormSubmissions.d.ts","sourceRoot":"","sources":["../../src/views/FormSubmissions.tsx"],"names":[],"mappings":"AAuCA,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAqED,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,oBAAoB,2CA+V3E"}
@@ -1,88 +1,235 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useMemo } from 'react';
4
- import { ArrowLeft, Download, Trash2, Search, Globe, ChevronDown, ChevronUp, MousePointerClick, MapPin, Smartphone, Monitor, Tablet, ExternalLink, Loader2, AlertTriangle, } from 'lucide-react';
3
+ /**
4
+ * Per-form submissions (entries) a compact, token-only table wired to the
5
+ * real entries API (`lib/forms-service.ts`). Replaces the previous card list.
6
+ *
7
+ * Features: summary chips, search + status pills (All / Unread / Starred /
8
+ * Archived), row selection with bulk actions (mark read, star, archive,
9
+ * delete), per-row star toggle, CSV export, and a detail drawer that opens on
10
+ * row click (marking the entry read). Loading / empty / filtered-empty / error
11
+ * states are all handled.
12
+ */
13
+ import { useCallback, useEffect, useState } from 'react';
14
+ import { ArrowLeft, Download, Inbox, Search, Star } from 'lucide-react';
5
15
  import { toast } from 'sonner';
6
- import { sortByRelevance } from '../lib/search.js';
7
- import { useApiData } from '../lib/useApiData.js';
8
- function sourceColor(source) {
9
- const s = (source ?? '').toLowerCase();
10
- if (s === 'google')
11
- return 'bg-blue-100 text-blue-800';
12
- if (s === 'facebook' || s === 'meta')
13
- return 'bg-indigo-100 text-indigo-800';
14
- if (s === 'linkedin')
15
- return 'bg-sky-100 text-sky-800';
16
- if (s === 'bing')
17
- return 'bg-teal-100 text-teal-800';
18
- if (s === '(direct)')
19
- return 'bg-gray-100 text-gray-800';
20
- return 'bg-purple-100 text-purple-800';
16
+ import { bulkUpdateEntries, deleteEntry, fetchEntries, fetchEntryCounts, fetchFormById, updateEntry, } from '../lib/forms-service.js';
17
+ import { emitFormsChanged, onFormsChanged } from '../lib/forms-events.js';
18
+ import { EntryDetailDrawer } from '../components/forms/EntryDetailDrawer.js';
19
+ import { ConfirmDialog } from '../components/ui/ConfirmDialog.js';
20
+ import { FormsEmptyState, FormsErrorState, FormsLoading, FormsPagination, SummaryChip, btnGhostIcon, btnSecondary, } from '../components/forms/primitives.js';
21
+ const FILTERS = [
22
+ { id: 'all', label: 'All' },
23
+ { id: 'unread', label: 'Unread' },
24
+ { id: 'starred', label: 'Starred' },
25
+ { id: 'archived', label: 'Archived' },
26
+ ];
27
+ const PAGE_SIZE = 25;
28
+ function filterToParams(filter) {
29
+ switch (filter) {
30
+ case 'unread':
31
+ return { unread: true };
32
+ case 'starred':
33
+ return { starred: true };
34
+ case 'archived':
35
+ return { archived: true };
36
+ default:
37
+ return {};
38
+ }
39
+ }
40
+ function formatDateTime(iso) {
41
+ if (!iso)
42
+ return '—';
43
+ const d = new Date(iso);
44
+ if (Number.isNaN(d.getTime()))
45
+ return '—';
46
+ return d.toLocaleString(undefined, {
47
+ month: 'short',
48
+ day: 'numeric',
49
+ year: 'numeric',
50
+ hour: 'numeric',
51
+ minute: '2-digit',
52
+ });
21
53
  }
22
- function mediumColor(medium) {
23
- const m = (medium ?? '').toLowerCase();
24
- if (m === 'cpc' || m === 'paid')
25
- return 'bg-orange-100 text-orange-800';
26
- if (m === 'organic')
27
- return 'bg-green-100 text-green-800';
28
- if (m === 'social')
29
- return 'bg-pink-100 text-pink-800';
30
- if (m === 'email')
31
- return 'bg-yellow-100 text-yellow-800';
32
- if (m === '(none)')
33
- return 'bg-gray-100 text-gray-600';
34
- return 'bg-gray-100 text-gray-800';
54
+ function csvEscape(value) {
55
+ const s = value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
56
+ return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
35
57
  }
36
- function DeviceIcon({ type }) {
37
- if (type === 'Mobile')
38
- return _jsx(Smartphone, { className: "h-3.5 w-3.5" });
39
- if (type === 'Tablet')
40
- return _jsx(Tablet, { className: "h-3.5 w-3.5" });
41
- return _jsx(Monitor, { className: "h-3.5 w-3.5" });
58
+ function exportCsv(entries, formSlug) {
59
+ const dataKeys = [...new Set(entries.flatMap((e) => Object.keys(e.data ?? {})))];
60
+ const header = ['Entry', 'Submitted', 'Name', 'Email', 'Phone', 'Status', ...dataKeys];
61
+ const lines = [header.map(csvEscape).join(',')];
62
+ for (const e of entries) {
63
+ const row = [
64
+ e.entryNumber ?? '',
65
+ e.submittedAt,
66
+ e.senderName ?? '',
67
+ e.senderEmail ?? '',
68
+ e.senderPhone ?? '',
69
+ e.unread ? 'unread' : 'read',
70
+ ...dataKeys.map((k) => e.data?.[k]),
71
+ ];
72
+ lines.push(row.map(csvEscape).join(','));
73
+ }
74
+ const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' });
75
+ const url = URL.createObjectURL(blob);
76
+ const a = document.createElement('a');
77
+ a.href = url;
78
+ a.download = `${formSlug || 'form'}-submissions.csv`;
79
+ a.click();
80
+ URL.revokeObjectURL(url);
42
81
  }
43
82
  export function FormSubmissions({ formId, onNavigate }) {
44
- const { data, loading, error, refetch } = useApiData(`/forms/${formId}/submissions`);
45
- const [searchQuery, setSearchQuery] = useState('');
46
- const [filterStatus, setFilterStatus] = useState('all');
47
- const [filterSource, setFilterSource] = useState('all');
48
- const [expandedId, setExpandedId] = useState(null);
49
- const submissions = Array.isArray(data) ? data : (data?.submissions ?? []);
50
- const filteredSubmissions = useMemo(() => {
51
- let results = submissions.filter((s) => {
52
- const matchesSearch = (s.name ?? '').toLowerCase().includes(searchQuery.toLowerCase()) ||
53
- (s.email ?? '').toLowerCase().includes(searchQuery.toLowerCase()) ||
54
- (s.message ?? '').toLowerCase().includes(searchQuery.toLowerCase());
55
- const matchesStatus = filterStatus === 'all' || s.status === filterStatus;
56
- const matchesSource = filterSource === 'all' || s.attribution?.source === filterSource;
57
- return matchesSearch && matchesStatus && matchesSource;
58
- });
59
- if (searchQuery.trim()) {
60
- results = sortByRelevance(results, searchQuery, (s) => [s.name, s.email, s.message]);
83
+ const [formName, setFormName] = useState('');
84
+ const [formSlug, setFormSlug] = useState('');
85
+ const [entries, setEntries] = useState([]);
86
+ const [total, setTotal] = useState(0);
87
+ const [counts, setCounts] = useState(null);
88
+ const [loading, setLoading] = useState(true);
89
+ const [error, setError] = useState(null);
90
+ const [search, setSearch] = useState('');
91
+ const [filter, setFilter] = useState('all');
92
+ const [page, setPage] = useState(1);
93
+ const [selected, setSelected] = useState(new Set());
94
+ const [activeEntry, setActiveEntry] = useState(null);
95
+ const [drawerOpen, setDrawerOpen] = useState(false);
96
+ const [confirmBulkDelete, setConfirmBulkDelete] = useState(false);
97
+ // Form metadata for the title (loads once).
98
+ useEffect(() => {
99
+ let cancelled = false;
100
+ void fetchFormById(formId)
101
+ .then((form) => {
102
+ if (cancelled || !form)
103
+ return;
104
+ setFormName(form.name);
105
+ setFormSlug(form.slug);
106
+ })
107
+ .catch(() => { });
108
+ return () => {
109
+ cancelled = true;
110
+ };
111
+ }, [formId]);
112
+ const loadCounts = useCallback(() => {
113
+ void fetchEntryCounts({ formId })
114
+ .then(setCounts)
115
+ .catch(() => { });
116
+ }, [formId]);
117
+ const load = useCallback(async () => {
118
+ setLoading(true);
119
+ setError(null);
120
+ try {
121
+ const result = await fetchEntries({
122
+ formId,
123
+ search: search.trim() || undefined,
124
+ page,
125
+ pageSize: PAGE_SIZE,
126
+ ...filterToParams(filter),
127
+ });
128
+ setEntries(result.entries);
129
+ setTotal(result.total);
130
+ setSelected(new Set());
61
131
  }
62
- return results;
63
- }, [submissions, searchQuery, filterStatus, filterSource]);
64
- const uniqueSources = [...new Set(submissions.map((s) => s.attribution?.source ?? '(direct)'))];
65
- const sourceSummary = useMemo(() => {
66
- const counts = {};
67
- for (const s of submissions) {
68
- const key = `${s.attribution?.source ?? '(direct)'} / ${s.attribution?.medium ?? '(none)'}`;
69
- counts[key] = (counts[key] ?? 0) + 1;
132
+ catch (e) {
133
+ setError(e instanceof Error ? e.message : 'Failed to load submissions');
70
134
  }
71
- return Object.entries(counts).sort(([, a], [, b]) => b - a);
72
- }, [submissions]);
73
- const paidCount = submissions.filter((s) => ['cpc', 'paid'].includes(s.attribution?.medium ?? '')).length;
74
- const organicCount = submissions.filter((s) => s.attribution?.medium === 'organic').length;
75
- const handleExport = () => {
76
- toast.success('Submissions exported to CSV (includes attribution data)');
77
- };
78
- if (loading) {
79
- return (_jsx("div", { className: "flex h-64 items-center justify-center p-3 pr-6 sm:p-4 sm:pr-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-blue-600" }) }));
80
- }
81
- return (_jsxs("div", { className: "p-3 pr-6 sm:p-4 sm:pr-8", children: [error && (_jsxs("div", { className: "mb-4 flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3", children: [_jsx(AlertTriangle, { className: "h-5 w-5 shrink-0 text-red-600" }), _jsx("span", { className: "flex-1 text-sm text-red-800", children: error }), _jsx("button", { onClick: refetch, className: "rounded-lg border border-red-300 px-3 py-1 text-sm text-red-700 transition-colors hover:bg-red-100", children: "Retry" })] })), _jsxs("div", { className: "mb-4", children: [_jsxs("button", { onClick: () => onNavigate?.('/forms'), className: "mb-3 inline-flex items-center gap-2 text-sm text-gray-600 transition-colors hover:text-gray-900", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to Forms"] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "mb-1 text-2xl font-semibold text-gray-900", children: "Contact Form - Submissions" }), _jsxs("p", { className: "text-sm text-gray-600", children: [filteredSubmissions.length, " submission", filteredSubmissions.length !== 1 ? 's' : ''] })] }), _jsxs("button", { onClick: handleExport, className: "flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700", children: [_jsx(Download, { className: "h-4 w-4" }), "Export CSV"] })] })] }), _jsxs("div", { className: "mb-4 grid grid-cols-2 gap-3 md:grid-cols-5", children: [_jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-4", children: [_jsx("div", { className: "mb-1 text-xs text-gray-600", children: "Total" }), _jsx("div", { className: "text-2xl font-semibold text-gray-900", children: submissions.length })] }), _jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-4", children: [_jsx("div", { className: "mb-1 text-xs text-gray-600", children: "New" }), _jsx("div", { className: "text-2xl font-semibold text-blue-600", children: submissions.filter((s) => s.status === 'new').length })] }), _jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-4", children: [_jsx("div", { className: "mb-1 text-xs text-gray-600", children: "Paid Traffic" }), _jsx("div", { className: "text-2xl font-semibold text-orange-600", children: paidCount })] }), _jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-4", children: [_jsx("div", { className: "mb-1 text-xs text-gray-600", children: "Organic" }), _jsx("div", { className: "text-2xl font-semibold text-green-600", children: organicCount })] }), _jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-4", children: [_jsx("div", { className: "mb-1 text-xs text-gray-600", children: "Top Source" }), _jsx("div", { className: "text-lg font-semibold text-gray-900", children: sourceSummary[0]?.[0] ?? '—' })] })] }), _jsxs("div", { className: "mb-4 rounded-lg border border-gray-200 bg-white p-4", children: [_jsxs("h3", { className: "mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900", children: [_jsx(Globe, { className: "h-4 w-4 text-blue-600" }), "Lead Source Breakdown"] }), _jsx("div", { className: "flex flex-wrap gap-2", children: sourceSummary.map(([label, count]) => (_jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5", children: [_jsx("span", { className: "text-sm font-medium text-gray-800", children: label }), _jsx("span", { className: "rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800", children: count })] }, label))) })] }), _jsx("div", { className: "mb-4 rounded-lg border border-gray-200 bg-white", children: _jsxs("div", { className: "flex flex-col gap-3 p-3 sm:flex-row", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" }), _jsx("input", { type: "text", placeholder: "Search submissions...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" })] }), _jsxs("select", { value: filterStatus, onChange: (e) => setFilterStatus(e.target.value), className: "rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none", children: [_jsx("option", { value: "all", children: "All Status" }), _jsx("option", { value: "new", children: "New" }), _jsx("option", { value: "read", children: "Read" }), _jsx("option", { value: "replied", children: "Replied" })] }), _jsxs("select", { value: filterSource, onChange: (e) => setFilterSource(e.target.value), className: "rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none", children: [_jsx("option", { value: "all", children: "All Sources" }), uniqueSources.map((src) => (_jsx("option", { value: src, children: src }, src)))] })] }) }), filteredSubmissions.length === 0 ? (_jsx("div", { className: "rounded-lg border border-gray-200 bg-white p-8 text-center", children: _jsx("p", { className: "text-sm text-gray-500", children: "No submissions yet" }) })) : (_jsx("div", { className: "space-y-3", children: filteredSubmissions.map((submission) => {
82
- const expanded = expandedId === submission.id;
83
- const attr = submission.attribution;
84
- const activeClickIds = Object.entries(attr.clickIds).filter(([, v]) => v);
85
- return (_jsxs("div", { className: "rounded-lg border border-gray-200 bg-white transition-shadow hover:shadow-md", children: [_jsxs("div", { className: "p-4", children: [_jsxs("div", { className: "mb-3 flex items-start justify-between", children: [_jsxs("div", { className: "flex flex-1 items-start gap-3", children: [_jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-linear-to-br from-blue-500 to-purple-600 text-sm font-medium text-white", children: submission.name.charAt(0) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "mb-1 flex items-center gap-2", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-900", children: submission.name }), _jsx("span", { className: `rounded-full px-2 py-0.5 text-xs font-medium ${submission.status === 'new' ? 'bg-blue-100 text-blue-800' : submission.status === 'read' ? 'bg-gray-100 text-gray-800' : 'bg-green-100 text-green-800'}`, children: submission.status })] }), _jsxs("div", { className: "mb-2 text-xs text-gray-600", children: [submission.email, submission.phone ? ` · ${submission.phone}` : ''] }), _jsx("p", { className: "text-sm text-gray-700", children: submission.message }), _jsxs("div", { className: "mt-2 text-xs text-gray-500", children: ["Submitted ", submission.submittedAt] })] })] }), _jsx("button", { onClick: () => toast.success('Submission deleted'), className: "rounded p-1.5 transition-colors hover:bg-gray-100", title: "Delete", children: _jsx(Trash2, { className: "h-4 w-4 text-red-600" }) })] }), _jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-2 border-t border-gray-100 pt-3", children: [_jsx("span", { className: `rounded-full px-2 py-0.5 text-xs font-medium ${sourceColor(attr.source)}`, children: attr.source }), _jsx("span", { className: "text-xs text-gray-400", children: "/" }), _jsx("span", { className: `rounded-full px-2 py-0.5 text-xs font-medium ${mediumColor(attr.medium)}`, children: attr.medium }), attr.campaign && (_jsx("span", { className: "rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800", children: attr.campaign })), _jsxs("span", { className: "flex items-center gap-1 text-xs text-gray-500", children: [_jsx(DeviceIcon, { type: attr.deviceType }), attr.deviceType] }), activeClickIds.length > 0 && (_jsxs("span", { className: "flex items-center gap-1 text-xs text-gray-500", children: [_jsx(MousePointerClick, { className: "h-3.5 w-3.5" }), activeClickIds.map(([k]) => k).join(', ')] })), _jsxs("button", { onClick: () => setExpandedId(expanded ? null : submission.id), className: "ml-auto flex items-center gap-1 text-xs text-blue-600 transition-colors hover:text-blue-800", children: [expanded ? (_jsx(ChevronUp, { className: "h-3.5 w-3.5" })) : (_jsx(ChevronDown, { className: "h-3.5 w-3.5" })), expanded ? 'Hide' : 'Attribution'] })] }), expanded && (_jsxs("div", { className: "mt-3 space-y-4 rounded-lg border-t border-gray-100 bg-gray-50 p-4 pt-3 text-sm", children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs("div", { children: [_jsx("h4", { className: "mb-2 text-xs font-semibold tracking-wider text-gray-600 uppercase", children: "Traffic Source" }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-gray-500", children: "Source" }), _jsx("span", { className: "font-medium text-gray-900", children: attr.source })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-gray-500", children: "Medium" }), _jsx("span", { className: "font-medium text-gray-900", children: attr.medium })] }), attr.campaign && (_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-gray-500", children: "Campaign" }), _jsx("span", { className: "font-medium text-gray-900", children: attr.campaign })] })), attr.term && (_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-gray-500", children: "Keyword" }), _jsx("span", { className: "font-medium text-gray-900", children: attr.term })] })), attr.content && (_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-gray-500", children: "Content" }), _jsx("span", { className: "font-medium text-gray-900", children: attr.content })] }))] })] }), _jsxs("div", { children: [_jsx("h4", { className: "mb-2 text-xs font-semibold tracking-wider text-gray-600 uppercase", children: "Page Context" }), _jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { children: [_jsx("span", { className: "text-xs text-gray-500", children: "Landing Page" }), _jsxs("div", { className: "mt-0.5 flex items-center gap-1", children: [_jsx(MapPin, { className: "h-3 w-3 shrink-0 text-gray-400" }), _jsx("span", { className: "text-xs font-medium break-all text-gray-900", children: attr.landingPage })] })] }), attr.referrer && (_jsxs("div", { children: [_jsx("span", { className: "text-xs text-gray-500", children: "Referrer" }), _jsxs("div", { className: "mt-0.5 flex items-center gap-1", children: [_jsx(ExternalLink, { className: "h-3 w-3 shrink-0 text-gray-400" }), _jsx("span", { className: "text-xs font-medium break-all text-gray-900", children: attr.referrer })] })] })), _jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-gray-500", children: "Device" }), _jsxs("span", { className: "flex items-center gap-1 font-medium text-gray-900", children: [_jsx(DeviceIcon, { type: attr.deviceType }), attr.deviceType] })] })] })] })] }), activeClickIds.length > 0 && (_jsxs("div", { children: [_jsx("h4", { className: "mb-2 text-xs font-semibold tracking-wider text-gray-600 uppercase", children: "Click IDs" }), _jsx("div", { className: "flex flex-wrap gap-2", children: activeClickIds.map(([key, val]) => (_jsxs("div", { className: "rounded-lg border border-gray-200 bg-white px-2.5 py-1", children: [_jsxs("span", { className: "text-xs text-gray-500", children: [key, ": "] }), _jsxs("span", { className: "font-mono text-xs text-gray-800", children: [String(val).slice(0, 20), "..."] })] }, key))) })] })), _jsxs("div", { className: "text-xs text-gray-500", children: ["Attribution captured ", attr.capturedAt] })] }))] }), _jsxs("div", { className: "flex items-center gap-2 border-t border-gray-200 px-4 py-3", children: [_jsx("button", { className: "rounded-lg bg-blue-600 px-3 py-1.5 text-sm text-white transition-colors hover:bg-blue-700", children: "Reply" }), _jsx("button", { className: "rounded-lg border border-gray-300 px-3 py-1.5 text-sm transition-colors hover:bg-gray-50", children: "Mark as Read" })] })] }, submission.id));
86
- }) }))] }));
135
+ finally {
136
+ setLoading(false);
137
+ }
138
+ }, [formId, search, filter, page]);
139
+ useEffect(() => {
140
+ void load();
141
+ }, [load]);
142
+ useEffect(() => {
143
+ loadCounts();
144
+ }, [loadCounts]);
145
+ // Refresh when another surface (drawer, sidebar) signals a change.
146
+ useEffect(() => onFormsChanged(() => {
147
+ loadCounts();
148
+ }), [loadCounts]);
149
+ const refresh = useCallback(() => {
150
+ void load();
151
+ loadCounts();
152
+ }, [load, loadCounts]);
153
+ const openEntry = useCallback((id) => {
154
+ setActiveEntry(id);
155
+ setDrawerOpen(true);
156
+ // Reflect read state immediately in the row; the drawer persists it.
157
+ setEntries((cur) => cur.map((e) => (e.id === id ? { ...e, unread: false } : e)));
158
+ }, []);
159
+ const toggleStar = useCallback(async (entry) => {
160
+ const next = !entry.starred;
161
+ setEntries((cur) => cur.map((e) => (e.id === entry.id ? { ...e, starred: next } : e)));
162
+ const res = await updateEntry(entry.id, { starred: next });
163
+ if (res.error) {
164
+ setEntries((cur) => cur.map((e) => (e.id === entry.id ? { ...e, starred: !next } : e)));
165
+ toast.error(res.error);
166
+ return;
167
+ }
168
+ emitFormsChanged();
169
+ }, []);
170
+ const allSelected = entries.length > 0 && selected.size === entries.length;
171
+ const toggleSelectAll = useCallback(() => {
172
+ setSelected((cur) => cur.size === entries.length ? new Set() : new Set(entries.map((e) => e.id)));
173
+ }, [entries]);
174
+ const toggleSelect = useCallback((id) => {
175
+ setSelected((cur) => {
176
+ const next = new Set(cur);
177
+ if (next.has(id))
178
+ next.delete(id);
179
+ else
180
+ next.add(id);
181
+ return next;
182
+ });
183
+ }, []);
184
+ const bulkPatch = useCallback(async (patch, label) => {
185
+ const ids = [...selected];
186
+ if (ids.length === 0)
187
+ return;
188
+ const res = await bulkUpdateEntries(ids, patch);
189
+ if (res.error) {
190
+ toast.error(res.error);
191
+ return;
192
+ }
193
+ toast.success(`${ids.length} ${ids.length === 1 ? 'entry' : 'entries'} ${label}`);
194
+ emitFormsChanged();
195
+ refresh();
196
+ }, [selected, refresh]);
197
+ const handleBulkDelete = useCallback(async () => {
198
+ const ids = [...selected];
199
+ if (ids.length === 0)
200
+ return;
201
+ const results = await Promise.allSettled(ids.map((id) => deleteEntry(id)));
202
+ const failed = results.filter((r) => r.status === 'rejected' || r.value.error).length;
203
+ if (failed > 0)
204
+ toast.error(`${failed} of ${ids.length} could not be deleted`);
205
+ else
206
+ toast.success(`${ids.length} ${ids.length === 1 ? 'entry' : 'entries'} deleted`);
207
+ emitFormsChanged();
208
+ refresh();
209
+ }, [selected, refresh]);
210
+ const showEmpty = !loading && !error && entries.length === 0;
211
+ const isFiltered = filter !== 'all' || search.trim().length > 0;
212
+ return (_jsxs("div", { className: "space-y-5 p-6", children: [_jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => onNavigate?.('/forms'), className: "text-muted-foreground hover:text-foreground mb-3 inline-flex items-center gap-2 text-sm transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4", "aria-hidden": true }), "Back to Forms"] }), _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-foreground text-2xl font-medium", children: formName ? `${formName} — Submissions` : 'Submissions' }), _jsxs("p", { className: "text-muted-foreground mt-1 text-sm", children: [total, " ", total === 1 ? 'submission' : 'submissions'] })] }), _jsxs("button", { type: "button", className: btnSecondary, onClick: () => exportCsv(entries, formSlug), disabled: entries.length === 0, children: [_jsx(Download, { className: "h-4 w-4", "aria-hidden": true }), "Export CSV"] })] })] }), counts && (_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(SummaryChip, { value: counts.total, label: "total" }), _jsx(SummaryChip, { value: counts.unread, label: "unread", tone: counts.unread > 0 ? 'destructive' : 'neutral' }), _jsx(SummaryChip, { value: counts.starred, label: "starred", tone: "warning" }), _jsx(SummaryChip, { value: counts.thisWeek, label: "this week" })] })), _jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "relative w-full sm:max-w-xs", children: [_jsx(Search, { className: "text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2", "aria-hidden": true }), _jsx("input", { type: "search", placeholder: "Search submissions\u2026", value: search, onChange: (e) => {
213
+ setSearch(e.target.value);
214
+ setPage(1);
215
+ }, "aria-label": "Search submissions", className: "border-border bg-input-background text-foreground focus-visible:ring-ring w-full rounded-lg border py-2 pr-3 pl-9 text-sm focus-visible:ring-2 focus-visible:outline-none" })] }), _jsx("div", { className: "flex gap-1", role: "group", "aria-label": "Filter submissions", children: FILTERS.map((f) => (_jsx("button", { type: "button", onClick: () => {
216
+ setFilter(f.id);
217
+ setPage(1);
218
+ }, "aria-pressed": filter === f.id, className: `rounded-full px-3 py-1.5 text-sm font-medium transition-colors ${filter === f.id
219
+ ? 'bg-primary text-primary-foreground'
220
+ : 'text-muted-foreground hover:bg-accent hover:text-foreground'}`, children: f.label }, f.id))) })] }), selected.size > 0 && (_jsxs("div", { className: "border-border bg-accent/40 flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2 text-sm", role: "region", "aria-label": "Bulk actions", children: [_jsxs("span", { className: "text-foreground font-medium", children: [selected.size, " selected"] }), _jsx("span", { className: "bg-border mx-1 h-4 w-px", "aria-hidden": true }), _jsx("button", { type: "button", className: "text-foreground hover:bg-accent rounded-md px-2 py-1 font-medium transition-colors", onClick: () => void bulkPatch({ unread: false }, 'marked read'), children: "Mark read" }), _jsx("button", { type: "button", className: "text-foreground hover:bg-accent rounded-md px-2 py-1 font-medium transition-colors", onClick: () => void bulkPatch({ starred: true }, 'starred'), children: "Star" }), _jsx("button", { type: "button", className: "text-foreground hover:bg-accent rounded-md px-2 py-1 font-medium transition-colors", onClick: () => void bulkPatch({ archived: filter !== 'archived' }, filter !== 'archived' ? 'archived' : 'unarchived'), children: filter === 'archived' ? 'Unarchive' : 'Archive' }), _jsx("button", { type: "button", className: "text-destructive hover:bg-destructive/10 rounded-md px-2 py-1 font-medium transition-colors", onClick: () => setConfirmBulkDelete(true), children: "Delete" }), _jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground ml-auto rounded-md px-2 py-1 font-medium transition-colors", onClick: () => setSelected(new Set()), children: "Clear" })] })), error ? (_jsx(FormsErrorState, { message: error, onRetry: load })) : loading ? (_jsx(FormsLoading, { label: "Loading submissions\u2026" })) : showEmpty ? (_jsx(FormsEmptyState, { icon: _jsx(Inbox, { className: "h-6 w-6", "aria-hidden": true }), title: isFiltered ? 'No submissions match your filters' : 'No submissions yet', description: isFiltered
221
+ ? 'Try a different search term or status filter.'
222
+ : 'Submissions will appear here once your form starts receiving entries.', action: isFiltered ? (_jsx("button", { type: "button", onClick: () => {
223
+ setSearch('');
224
+ setFilter('all');
225
+ setPage(1);
226
+ }, className: "text-primary text-sm font-medium hover:underline", children: "Clear filters" })) : undefined })) : (_jsx(SubmissionsTable, { entries: entries, selected: selected, allSelected: allSelected, onToggleSelectAll: toggleSelectAll, onToggleSelect: toggleSelect, onOpen: openEntry, onToggleStar: toggleStar, footer: _jsx(FormsPagination, { page: page, pageSize: PAGE_SIZE, total: total, onPageChange: setPage, noun: "submission", label: "Submissions pagination" }) })), _jsx(EntryDetailDrawer, { entryId: activeEntry, open: drawerOpen, onOpenChange: setDrawerOpen, onChanged: refresh, onDeleted: () => refresh() }), _jsx(ConfirmDialog, { open: confirmBulkDelete, onClose: () => setConfirmBulkDelete(false), onConfirm: () => void handleBulkDelete(), title: `Delete ${selected.size} ${selected.size === 1 ? 'submission' : 'submissions'}?`, description: "This permanently removes the selected submissions and any attached files. This cannot be undone.", confirmLabel: "Delete", destructive: true })] }));
227
+ }
228
+ function SubmissionsTable({ entries, selected, allSelected, onToggleSelectAll, onToggleSelect, onOpen, onToggleStar, footer, }) {
229
+ return (_jsxs("div", { className: "border-border bg-card rounded-lg border", children: [_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[720px] text-sm", "aria-label": "Submissions", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-border text-muted-foreground border-b text-left text-xs", children: [_jsx("th", { scope: "col", className: "w-10 px-4 py-2.5", children: _jsx("input", { type: "checkbox", checked: allSelected, onChange: onToggleSelectAll, "aria-label": "Select all submissions", className: "border-border text-primary focus-visible:ring-ring h-4 w-4 rounded" }) }), _jsx("th", { scope: "col", className: "px-4 py-2.5 font-medium", children: "Sender" }), _jsx("th", { scope: "col", className: "px-4 py-2.5 font-medium", children: "Submission" }), _jsx("th", { scope: "col", className: "px-4 py-2.5 font-medium whitespace-nowrap", children: "Submitted" }), _jsx("th", { scope: "col", className: "w-10 px-4 py-2.5 text-center font-medium", children: _jsx("span", { className: "sr-only", children: "Starred" }) })] }) }), _jsx("tbody", { children: entries.map((entry) => {
230
+ const isSelected = selected.has(entry.id);
231
+ const name = entry.senderName || 'Anonymous';
232
+ return (_jsxs("tr", { className: `border-border hover:bg-accent/40 group cursor-pointer border-b transition-colors last:border-b-0 ${isSelected ? 'bg-accent/30' : ''}`, onClick: () => onOpen(entry.id), children: [_jsx("td", { className: "px-4 py-3", onClick: (e) => e.stopPropagation(), children: _jsx("input", { type: "checkbox", checked: isSelected, onChange: () => onToggleSelect(entry.id), "aria-label": `Select submission from ${name}`, className: "border-border text-primary focus-visible:ring-ring h-4 w-4 rounded" }) }), _jsx("td", { className: "px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "bg-muted text-muted-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-medium", "aria-hidden": true, children: name.charAt(0).toUpperCase() }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [entry.unread && (_jsx("span", { className: "bg-primary h-1.5 w-1.5 shrink-0 rounded-full", "aria-label": "Unread" })), _jsx("span", { className: `text-foreground truncate ${entry.unread ? 'font-semibold' : 'font-medium'}`, children: name })] }), entry.senderEmail && (_jsx("span", { className: "text-muted-foreground block truncate text-xs", children: entry.senderEmail }))] })] }) }), _jsx("td", { className: "px-4 py-3", children: _jsx("span", { className: "text-muted-foreground line-clamp-1 max-w-md", children: entry.preview || '—' }) }), _jsx("td", { className: "text-muted-foreground px-4 py-3 whitespace-nowrap", children: formatDateTime(entry.submittedAt) }), _jsx("td", { className: "px-4 py-3 text-center", onClick: (e) => e.stopPropagation(), children: _jsx("button", { type: "button", className: btnGhostIcon, "aria-pressed": entry.starred, "aria-label": entry.starred ? 'Unstar submission' : 'Star submission', onClick: () => onToggleStar(entry), children: _jsx(Star, { className: `h-4 w-4 ${entry.starred ? 'fill-warning text-warning' : ''}`, "aria-hidden": true }) }) })] }, entry.id));
233
+ }) })] }) }), footer] }));
87
234
  }
88
235
  //# sourceMappingURL=FormSubmissions.js.map