larxius commited on
Commit
0f3e5e4
·
verified ·
1 Parent(s): 0b13263

Update frontend/src/pages/SuperAdminPanel.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/SuperAdminPanel.jsx +245 -56
frontend/src/pages/SuperAdminPanel.jsx CHANGED
@@ -334,30 +334,60 @@ const SuperAdminPanel = () => {
334
  const [bookingSearch, setBookingSearch] = useState('');
335
  const [bookingStatusFilter, setBookingStatusFilter] = useState('all');
336
 
337
- const getFilteredBookings = () => {
338
- return (demoBookings || []).filter(b => {
339
- const emailMatch = !bookingSearch ||
340
- (b.email || '').toLowerCase().includes(bookingSearch.toLowerCase()) ||
341
- (b.company_size || '').toLowerCase().includes(bookingSearch.toLowerCase()) ||
342
- (b.meeting_date || '').toLowerCase().includes(bookingSearch.toLowerCase());
343
-
344
- const statusMatch = bookingStatusFilter === 'all' || (b.status || 'pending') === bookingStatusFilter;
345
- return emailMatch && statusMatch;
 
 
 
 
346
  });
347
  };
348
 
349
- const handleOrgSort = (column) => {
350
- if (column === 'Quotas' || column === 'Actions') return;
351
- if (sortOrgCol === column) {
352
- setSortOrgDir(sortOrgDir === 'asc' ? 'desc' : 'asc');
353
- } else {
354
- setSortOrgCol(column);
355
- setSortOrgDir('asc');
356
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  };
358
 
359
  const getSortedOrgs = () => {
360
- return [...(organizations || [])].sort((a, b) => {
 
361
  let aVal, bVal;
362
  switch (sortOrgCol) {
363
  case 'Tenant Name': aVal = a.name || ''; bVal = b.name || ''; break;
@@ -371,6 +401,34 @@ const SuperAdminPanel = () => {
371
  });
372
  };
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  const handleUserSort = (column) => {
375
  if (column === 'Actions') return;
376
  if (sortUserCol === column) {
@@ -440,23 +498,6 @@ const SuperAdminPanel = () => {
440
  }
441
  };
442
 
443
- const getSortedBills = () => {
444
- return [...(recentPayments || [])].sort((a, b) => {
445
- let aVal, bVal;
446
- switch (sortBillCol) {
447
- case 'Date': aVal = new Date(a.created_at || Date.now()).getTime(); bVal = new Date(b.created_at || Date.now()).getTime(); break;
448
- case 'Organization / User': aVal = a.org_name || ''; bVal = b.org_name || ''; break;
449
- case 'Tier': aVal = a.tier_id || ''; bVal = b.tier_id || ''; break;
450
- case 'Amount': aVal = a.amount || 0; bVal = b.amount || 0; break;
451
- case 'Status': aVal = a.status || ''; bVal = b.status || ''; break;
452
- default: return 0;
453
- }
454
- if (aVal < bVal) return sortBillDir === 'asc' ? -1 : 1;
455
- if (aVal > bVal) return sortBillDir === 'asc' ? 1 : -1;
456
- return 0;
457
- });
458
- };
459
-
460
  const [metrics, setMetrics] = useState({
461
  total_tenants: 0,
462
  active_licenses: 0,
@@ -1306,9 +1347,38 @@ const SuperAdminPanel = () => {
1306
  </div>
1307
 
1308
  <div className="lg:col-span-2">
1309
- <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] mt-xl mb-md">
1310
- <span className="material-symbols-outlined text-primary mr-2 text-[20px]">receipt_long</span> Global Transaction History
1311
- </h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1312
  <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
1313
  {loading ? <div className="p-10 text-center text-on-surface-variant text-[14px]">Fetching logs...</div> : (
1314
  <div className="overflow-x-auto max-h-[400px] hide-scrollbar">
@@ -1404,7 +1474,62 @@ const SuperAdminPanel = () => {
1404
 
1405
  {activeTab === 'organizations' && (
1406
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
1407
- <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] mb-md"><Shield className="w-5 h-5 text-primary mr-2" /> Client Organizations Directory</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1408
  <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto hide-scrollbar">
1409
  {loading ? <div className="p-10 text-center text-on-surface-variant">Fetching directory...</div> : (
1410
  <table className="w-full text-left text-sm border-collapse">
@@ -1688,7 +1813,27 @@ const SuperAdminPanel = () => {
1688
 
1689
  {activeTab === 'audit' && (
1690
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
1691
- <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] mb-md"><span className="material-symbols-outlined text-primary mr-2">history</span> Full System Audit Logs</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1692
  <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
1693
  {loading ? <div className="p-10 text-center text-on-surface-variant">Fetching logs...</div> : (
1694
  <>
@@ -1703,22 +1848,26 @@ const SuperAdminPanel = () => {
1703
  </tr>
1704
  </thead>
1705
  <tbody className="divide-y divide-outline-variant">
1706
- {auditLogs.length === 0 ? <tr><td colSpan="4" className="p-10 text-center text-on-surface-variant">No audit logs found.</td></tr> : auditLogs.slice((auditPage - 1) * auditPageSize, auditPage * auditPageSize).map((log) => (
1707
- <tr key={log.id} className="hover:bg-surface-container transition-colors">
1708
- <td className="px-md py-sm text-[13px] text-on-surface-variant">{new Date(log.created_at || log.timestamp).toLocaleString()}</td>
1709
- <td className="px-md py-sm"><div className="font-bold text-on-surface text-[13px]">{log.user_email}</div></td>
1710
- <td className="px-md py-sm text-[13px] text-on-surface-variant font-medium">
1711
- {log.action.includes('Terminated') ? <span className="text-error font-bold">{log.action}</span> : log.action}
1712
- </td>
1713
- <td className="px-md py-sm font-bold text-[13px] text-on-surface">{log.target_name || log.target_id || '-'}</td>
1714
- </tr>
1715
- ))}
 
 
 
 
1716
  </tbody>
1717
  </table>
1718
  </div>
1719
  <TablePagination
1720
  currentPage={auditPage}
1721
- totalEntries={auditLogs.length}
1722
  pageSize={auditPageSize}
1723
  onPageChange={setAuditPage}
1724
  onPageSizeChange={setAuditPageSize}
@@ -1892,10 +2041,50 @@ const SuperAdminPanel = () => {
1892
 
1893
  {activeTab === 'emails' && (
1894
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
1895
- <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] mb-md"><span className="material-symbols-outlined text-primary mr-2">mail</span> Outbound Email Logs</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1896
  <div className="bg-surface border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
1897
- {emailLogs.length === 0 ? (
1898
- <div className="p-xl text-center text-on-surface-variant font-bold">No emails sent yet.</div>
1899
  ) : (
1900
  <>
1901
  <div className="overflow-x-auto">
@@ -1909,7 +2098,7 @@ const SuperAdminPanel = () => {
1909
  </tr>
1910
  </thead>
1911
  <tbody className="divide-y divide-outline-variant">
1912
- {emailLogs.slice((emailPage - 1) * emailPageSize, emailPage * emailPageSize).map(log => (
1913
  <tr key={log.id} className="hover:bg-surface-container-lowest transition-colors">
1914
  <td className="px-md py-sm text-on-surface-variant text-[13px]">{new Date(log.sent_at).toLocaleString()}</td>
1915
  <td className="px-md py-sm font-bold text-on-surface text-[13px]">{log.recipient}</td>
@@ -1926,7 +2115,7 @@ const SuperAdminPanel = () => {
1926
  </div>
1927
  <TablePagination
1928
  currentPage={emailPage}
1929
- totalEntries={emailLogs.length}
1930
  pageSize={emailPageSize}
1931
  onPageChange={setEmailPage}
1932
  onPageSizeChange={setEmailPageSize}
 
334
  const [bookingSearch, setBookingSearch] = useState('');
335
  const [bookingStatusFilter, setBookingStatusFilter] = useState('all');
336
 
337
+ // Payments Filtering
338
+ const [paymentSearch, setPaymentSearch] = useState('');
339
+ const [paymentStatusFilter, setPaymentStatusFilter] = useState('all');
340
+
341
+ const getFilteredBills = () => {
342
+ return (recentPayments || []).filter(p => {
343
+ const q = paymentSearch.toLowerCase().trim();
344
+ const matchSearch = !paymentSearch ||
345
+ (p.org_name || '').toLowerCase().includes(q) ||
346
+ (p.stripe_payment_id || p.id || '').toLowerCase().includes(q) ||
347
+ (p.tier_id || '').toLowerCase().includes(q);
348
+ const matchStatus = paymentStatusFilter === 'all' || (p.status || '').toLowerCase() === paymentStatusFilter.toLowerCase();
349
+ return matchSearch && matchStatus;
350
  });
351
  };
352
 
353
+ const getSortedBills = () => {
354
+ const filtered = getFilteredBills();
355
+ return [...filtered].sort((a, b) => {
356
+ let aVal, bVal;
357
+ switch (sortBillCol) {
358
+ case 'Date': aVal = new Date(a.created_at || Date.now()).getTime(); bVal = new Date(b.created_at || Date.now()).getTime(); break;
359
+ case 'Organization / User': aVal = a.org_name || ''; bVal = b.org_name || ''; break;
360
+ case 'Tier': aVal = a.tier_id || ''; bVal = b.tier_id || ''; break;
361
+ case 'Amount': aVal = a.amount || 0; bVal = b.amount || 0; break;
362
+ case 'Status': aVal = a.status || ''; bVal = b.status || ''; break;
363
+ default: return 0;
364
+ }
365
+ if (aVal < bVal) return sortBillDir === 'asc' ? -1 : 1;
366
+ if (aVal > bVal) return sortBillDir === 'asc' ? 1 : -1;
367
+ return 0;
368
+ });
369
+ };
370
+
371
+ // Organizations Filtering
372
+ const [orgSearch, setOrgSearch] = useState('');
373
+ const [orgTierFilter, setOrgTierFilter] = useState('all');
374
+ const [orgStatusFilter, setOrgStatusFilter] = useState('all');
375
+
376
+ const getFilteredOrgs = () => {
377
+ return (organizations || []).filter(org => {
378
+ const q = orgSearch.toLowerCase().trim();
379
+ const matchSearch = !orgSearch || (org.name || '').toLowerCase().includes(q) || (org.id || '').toLowerCase().includes(q);
380
+ const rawTier = (org.tier || org.subscription_tier || '').toLowerCase();
381
+ const matchTier = orgTierFilter === 'all' || rawTier.includes(orgTierFilter.toLowerCase());
382
+ const orgStatus = (org.status || (org.is_active ? 'active' : 'inactive')).toLowerCase();
383
+ const matchStatus = orgStatusFilter === 'all' || orgStatus === orgStatusFilter.toLowerCase();
384
+ return matchSearch && matchTier && matchStatus;
385
+ });
386
  };
387
 
388
  const getSortedOrgs = () => {
389
+ const filtered = getFilteredOrgs();
390
+ return [...filtered].sort((a, b) => {
391
  let aVal, bVal;
392
  switch (sortOrgCol) {
393
  case 'Tenant Name': aVal = a.name || ''; bVal = b.name || ''; break;
 
401
  });
402
  };
403
 
404
+ // Audit Logs Filtering
405
+ const [auditSearch, setAuditSearch] = useState('');
406
+
407
+ const getFilteredAuditLogs = () => {
408
+ return (auditLogs || []).filter(log => {
409
+ if (!auditSearch) return true;
410
+ const q = auditSearch.toLowerCase().trim();
411
+ return (log.action || '').toLowerCase().includes(q) ||
412
+ (log.user_email || log.admin_id || '').toLowerCase().includes(q) ||
413
+ (log.target_name || log.target_id || '').toLowerCase().includes(q);
414
+ });
415
+ };
416
+
417
+ // Outbound Email Logs Filtering
418
+ const [emailSearch, setEmailSearch] = useState('');
419
+ const [emailStatusFilter, setEmailStatusFilter] = useState('all');
420
+
421
+ const getFilteredEmails = () => {
422
+ return (emailLogs || []).filter(e => {
423
+ const q = emailSearch.toLowerCase().trim();
424
+ const matchSearch = !emailSearch ||
425
+ (e.recipient || '').toLowerCase().includes(q) ||
426
+ (e.subject || '').toLowerCase().includes(q);
427
+ const matchStatus = emailStatusFilter === 'all' || (e.status || '').toLowerCase() === emailStatusFilter.toLowerCase();
428
+ return matchSearch && matchStatus;
429
+ });
430
+ };
431
+
432
  const handleUserSort = (column) => {
433
  if (column === 'Actions') return;
434
  if (sortUserCol === column) {
 
498
  }
499
  };
500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  const [metrics, setMetrics] = useState({
502
  total_tenants: 0,
503
  active_licenses: 0,
 
1347
  </div>
1348
 
1349
  <div className="lg:col-span-2">
1350
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-2 mt-xl mb-md">
1351
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] m-0">
1352
+ <span className="material-symbols-outlined text-primary mr-2 text-[20px]">receipt_long</span> Global Transaction History
1353
+ </h2>
1354
+ <div className="flex gap-2 flex-wrap items-center">
1355
+ <div className="relative">
1356
+ <Search className="w-3.5 h-3.5 absolute left-2.5 top-2.5 text-on-surface-variant" />
1357
+ <input
1358
+ type="text"
1359
+ placeholder="Search org, tier..."
1360
+ value={paymentSearch}
1361
+ onChange={e => setPaymentSearch(e.target.value)}
1362
+ className="bg-surface-container border border-outline-variant/60 rounded-lg pl-8 pr-7 py-1 text-[12.5px] outline-none text-on-surface w-48"
1363
+ />
1364
+ {paymentSearch && (
1365
+ <button onClick={() => setPaymentSearch('')} className="absolute right-2 top-2 text-on-surface-variant hover:text-on-surface border-0 bg-transparent cursor-pointer">
1366
+ <X className="w-3 h-3" />
1367
+ </button>
1368
+ )}
1369
+ </div>
1370
+ <select
1371
+ value={paymentStatusFilter}
1372
+ onChange={e => setPaymentStatusFilter(e.target.value)}
1373
+ className="bg-surface-container border border-outline-variant/60 text-on-surface text-[12.5px] font-medium rounded-lg px-2.5 py-1 outline-none cursor-pointer"
1374
+ >
1375
+ <option value="all">All Statuses</option>
1376
+ <option value="succeeded">Succeeded / Paid</option>
1377
+ <option value="failed">Failed</option>
1378
+ <option value="pending">Pending</option>
1379
+ </select>
1380
+ </div>
1381
+ </div>
1382
  <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
1383
  {loading ? <div className="p-10 text-center text-on-surface-variant text-[14px]">Fetching logs...</div> : (
1384
  <div className="overflow-x-auto max-h-[400px] hide-scrollbar">
 
1474
 
1475
  {activeTab === 'organizations' && (
1476
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
1477
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-md mb-md">
1478
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] m-0">
1479
+ <Shield className="w-5 h-5 text-primary mr-2" /> Client Organizations Directory
1480
+ </h2>
1481
+
1482
+ <div className="flex items-center gap-2 flex-wrap">
1483
+ <div className="relative">
1484
+ <Search className="w-4 h-4 absolute left-3 top-2 text-on-surface-variant" />
1485
+ <input
1486
+ type="text"
1487
+ placeholder="Search tenant name..."
1488
+ value={orgSearch}
1489
+ onChange={e => { setOrgSearch(e.target.value); setOrgPage(1); }}
1490
+ className="bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg pl-9 pr-8 py-1.5 outline-none focus:border-primary w-48"
1491
+ />
1492
+ {orgSearch && (
1493
+ <button onClick={() => { setOrgSearch(''); setOrgPage(1); }} className="absolute right-2.5 top-2 text-on-surface-variant hover:text-on-surface border-0 bg-transparent cursor-pointer">
1494
+ <X className="w-3.5 h-3.5" />
1495
+ </button>
1496
+ )}
1497
+ </div>
1498
+
1499
+ <select
1500
+ value={orgTierFilter}
1501
+ onChange={e => { setOrgTierFilter(e.target.value); setOrgPage(1); }}
1502
+ className="bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg px-3 py-1.5 outline-none focus:border-primary cursor-pointer"
1503
+ >
1504
+ <option value="all">All Tiers</option>
1505
+ <option value="quick">Quick</option>
1506
+ <option value="advanced">Advanced</option>
1507
+ <option value="deep">Deep</option>
1508
+ <option value="enterprise">Enterprise</option>
1509
+ <option value="free">Free</option>
1510
+ </select>
1511
+
1512
+ <select
1513
+ value={orgStatusFilter}
1514
+ onChange={e => { setOrgStatusFilter(e.target.value); setOrgPage(1); }}
1515
+ className="bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg px-3 py-1.5 outline-none focus:border-primary cursor-pointer"
1516
+ >
1517
+ <option value="all">All Statuses</option>
1518
+ <option value="active">Active</option>
1519
+ <option value="inactive">Inactive</option>
1520
+ </select>
1521
+
1522
+ {(orgSearch || orgTierFilter !== 'all' || orgStatusFilter !== 'all') && (
1523
+ <button
1524
+ onClick={() => { setOrgSearch(''); setOrgTierFilter('all'); setOrgStatusFilter('all'); setOrgPage(1); }}
1525
+ className="text-error hover:underline text-[12.5px] font-bold flex items-center gap-1 cursor-pointer bg-transparent border-0 px-1"
1526
+ >
1527
+ <X className="w-4 h-4" />
1528
+ Reset
1529
+ </button>
1530
+ )}
1531
+ </div>
1532
+ </div>
1533
  <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto hide-scrollbar">
1534
  {loading ? <div className="p-10 text-center text-on-surface-variant">Fetching directory...</div> : (
1535
  <table className="w-full text-left text-sm border-collapse">
 
1813
 
1814
  {activeTab === 'audit' && (
1815
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
1816
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-md mb-md">
1817
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] m-0">
1818
+ <span className="material-symbols-outlined text-primary mr-2">history</span> Full System Audit Logs
1819
+ </h2>
1820
+ <div className="relative w-full md:w-72">
1821
+ <Search className="w-4 h-4 absolute left-3 top-2.5 text-on-surface-variant" />
1822
+ <input
1823
+ type="text"
1824
+ placeholder="Search event, admin email, or target..."
1825
+ value={auditSearch}
1826
+ onChange={e => { setAuditSearch(e.target.value); setAuditPage(1); }}
1827
+ className="w-full bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg pl-9 pr-8 py-1.5 outline-none focus:border-primary"
1828
+ />
1829
+ {auditSearch && (
1830
+ <button onClick={() => { setAuditSearch(''); setAuditPage(1); }} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-on-surface-variant hover:text-on-surface border-0 bg-transparent cursor-pointer p-0.5">
1831
+ <X className="w-3.5 h-3.5" />
1832
+ </button>
1833
+ )}
1834
+ </div>
1835
+ </div>
1836
+
1837
  <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
1838
  {loading ? <div className="p-10 text-center text-on-surface-variant">Fetching logs...</div> : (
1839
  <>
 
1848
  </tr>
1849
  </thead>
1850
  <tbody className="divide-y divide-outline-variant">
1851
+ {getFilteredAuditLogs().length === 0 ? (
1852
+ <tr><td colSpan="4" className="p-10 text-center text-on-surface-variant font-bold">No audit logs match your search.</td></tr>
1853
+ ) : (
1854
+ getFilteredAuditLogs().slice((auditPage - 1) * auditPageSize, auditPage * auditPageSize).map((log) => (
1855
+ <tr key={log.id} className="hover:bg-surface-container transition-colors">
1856
+ <td className="px-md py-sm text-[13px] text-on-surface-variant">{new Date(log.created_at || log.timestamp).toLocaleString()}</td>
1857
+ <td className="px-md py-sm"><div className="font-bold text-on-surface text-[13px]">{log.user_email}</div></td>
1858
+ <td className="px-md py-sm text-[13px] text-on-surface-variant font-medium">
1859
+ {log.action.includes('Terminated') ? <span className="text-error font-bold">{log.action}</span> : log.action}
1860
+ </td>
1861
+ <td className="px-md py-sm font-bold text-[13px] text-on-surface">{log.target_name || log.target_id || '-'}</td>
1862
+ </tr>
1863
+ ))
1864
+ )}
1865
  </tbody>
1866
  </table>
1867
  </div>
1868
  <TablePagination
1869
  currentPage={auditPage}
1870
+ totalEntries={getFilteredAuditLogs().length}
1871
  pageSize={auditPageSize}
1872
  onPageChange={setAuditPage}
1873
  onPageSizeChange={setAuditPageSize}
 
2041
 
2042
  {activeTab === 'emails' && (
2043
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
2044
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-md mb-md">
2045
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] m-0">
2046
+ <span className="material-symbols-outlined text-primary mr-2">mail</span> Outbound Email Logs
2047
+ </h2>
2048
+ <div className="flex items-center gap-2 flex-wrap">
2049
+ <div className="relative">
2050
+ <Search className="w-4 h-4 absolute left-3 top-2.5 text-on-surface-variant" />
2051
+ <input
2052
+ type="text"
2053
+ placeholder="Search recipient or subject..."
2054
+ value={emailSearch}
2055
+ onChange={e => { setEmailSearch(e.target.value); setEmailPage(1); }}
2056
+ className="bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg pl-9 pr-8 py-1.5 outline-none focus:border-primary w-56"
2057
+ />
2058
+ {emailSearch && (
2059
+ <button onClick={() => { setEmailSearch(''); setEmailPage(1); }} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-on-surface-variant hover:text-on-surface border-0 bg-transparent cursor-pointer p-0.5">
2060
+ <X className="w-3.5 h-3.5" />
2061
+ </button>
2062
+ )}
2063
+ </div>
2064
+ <select
2065
+ value={emailStatusFilter}
2066
+ onChange={e => { setEmailStatusFilter(e.target.value); setEmailPage(1); }}
2067
+ className="bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg px-3 py-1.5 outline-none focus:border-primary cursor-pointer"
2068
+ >
2069
+ <option value="all">All Statuses</option>
2070
+ <option value="sent">Sent</option>
2071
+ <option value="failed">Failed</option>
2072
+ </select>
2073
+ {(emailSearch || emailStatusFilter !== 'all') && (
2074
+ <button
2075
+ onClick={() => { setEmailSearch(''); setEmailStatusFilter('all'); setEmailPage(1); }}
2076
+ className="text-error hover:underline text-[12.5px] font-bold flex items-center gap-1 cursor-pointer bg-transparent border-0 px-1"
2077
+ >
2078
+ <X className="w-4 h-4" />
2079
+ Reset
2080
+ </button>
2081
+ )}
2082
+ </div>
2083
+ </div>
2084
+
2085
  <div className="bg-surface border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
2086
+ {getFilteredEmails().length === 0 ? (
2087
+ <div className="p-xl text-center text-on-surface-variant font-bold">No emails match your filter criteria.</div>
2088
  ) : (
2089
  <>
2090
  <div className="overflow-x-auto">
 
2098
  </tr>
2099
  </thead>
2100
  <tbody className="divide-y divide-outline-variant">
2101
+ {getFilteredEmails().slice((emailPage - 1) * emailPageSize, emailPage * emailPageSize).map(log => (
2102
  <tr key={log.id} className="hover:bg-surface-container-lowest transition-colors">
2103
  <td className="px-md py-sm text-on-surface-variant text-[13px]">{new Date(log.sent_at).toLocaleString()}</td>
2104
  <td className="px-md py-sm font-bold text-on-surface text-[13px]">{log.recipient}</td>
 
2115
  </div>
2116
  <TablePagination
2117
  currentPage={emailPage}
2118
+ totalEntries={getFilteredEmails().length}
2119
  pageSize={emailPageSize}
2120
  onPageChange={setEmailPage}
2121
  onPageSizeChange={setEmailPageSize}