Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update frontend/src/pages/SuperAdminPanel.jsx
Browse files
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 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
const
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
});
|
| 347 |
};
|
| 348 |
|
| 349 |
-
const
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
};
|
| 358 |
|
| 359 |
const getSortedOrgs = () => {
|
| 360 |
-
|
|
|
|
| 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 |
-
<
|
| 1310 |
-
<
|
| 1311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
{
|
| 1707 |
-
<tr
|
| 1708 |
-
|
| 1709 |
-
|
| 1710 |
-
<
|
| 1711 |
-
|
| 1712 |
-
|
| 1713 |
-
|
| 1714 |
-
|
| 1715 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1716 |
</tbody>
|
| 1717 |
</table>
|
| 1718 |
</div>
|
| 1719 |
<TablePagination
|
| 1720 |
currentPage={auditPage}
|
| 1721 |
-
totalEntries={
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1896 |
<div className="bg-surface border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
|
| 1897 |
-
{
|
| 1898 |
-
<div className="p-xl text-center text-on-surface-variant font-bold">No emails
|
| 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 |
-
{
|
| 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={
|
| 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}
|