diff --git a/frontend/src/screens/Inbox.jsx b/frontend/src/screens/Inbox.jsx
index 7516af3..ea14593 100644
--- a/frontend/src/screens/Inbox.jsx
+++ b/frontend/src/screens/Inbox.jsx
@@ -1044,6 +1044,63 @@ export default function Inbox() {
markDuplicate.mutate({ id: item.id, isDuplicate: !item.duplicate, kind: item.kind })
}
+ const [exporting, setExporting] = useState(false)
+
+ /** CSV of the current view — channel, tab and search respected, unpaged. */
+ async function exportCsv() {
+ if (exporting) return
+ setExporting(true)
+ try {
+ const search = q.trim() ? { search: q.trim() } : {}
+ const [emailRes, formRes] = await Promise.all([
+ !isForms
+ ? fetchApplications({ ...tabFilter, ...search })
+ : Promise.resolve({ rows: [] }),
+ isForms || isAllChannel
+ ? fetchFormApplications({
+ sheet: isAllChannel ? undefined : (formSheet || undefined),
+ ...formTabFilter,
+ ...search,
+ })
+ : Promise.resolve({ rows: [] }),
+ ])
+ const rows = [...(emailRes.rows ?? []), ...(formRes.rows ?? [])]
+ .sort((a, b) => (b.received?.getTime() ?? 0) - (a.received?.getTime() ?? 0))
+ if (!rows.length) {
+ toast('Nothing to export in this view', 'info')
+ return
+ }
+ const esc = (v) => {
+ const s = v == null ? '' : String(v)
+ return /[",\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s
+ }
+ const header = ['Name', 'Email', 'Phone', 'Position', 'Channel', 'Source', 'Received', 'Status', 'City', 'Notice period', 'ATS score', 'Assigned job']
+ const lines = [header.join(',')]
+ for (const r of rows) {
+ lines.push([
+ r.name, r.email, r.phone, r.position,
+ r.kind === 'form' ? 'Sheet Form' : 'Email',
+ r.source,
+ r.received ? r.received.toISOString().slice(0, 10) : '',
+ r.processing, r.residingCity, r.noticePeriod, r.atsScore,
+ r.assignedPost?.title,
+ ].map(esc).join(','))
+ }
+ // BOM so Excel opens it as UTF-8 rather than mangling names.
+ const blob = new Blob([String.fromCharCode(0xFEFF) + lines.join('\n')], { type: 'text/csv;charset=utf-8' })
+ const a = document.createElement('a')
+ a.href = URL.createObjectURL(blob)
+ a.download = `inbox-${channel}-${tab.toLowerCase().replaceAll(' ', '-')}-${new Date().toISOString().slice(0, 10)}.csv`
+ a.click()
+ URL.revokeObjectURL(a.href)
+ toast(`Exported ${rows.length} application${rows.length === 1 ? '' : 's'}`, 'success')
+ } catch (err) {
+ toast(friendlyAuthError(err, 'Export failed'), 'error')
+ } finally {
+ setExporting(false)
+ }
+ }
+
const [syncRunId, setSyncRunId] = useState(() => readStoredSyncRunId())
const syncToastShown = useRef(null)
@@ -1135,6 +1192,9 @@ export default function Inbox() {
onClick={() => sync.mutate()}
/>
)}
+
@@ -1212,11 +1272,31 @@ export default function Inbox() {
{activeQuery.isPending && (