larxius commited on
Commit
8eb59ca
·
verified ·
1 Parent(s): 69df7c3

Update frontend/src/pages/SuperAdminPanel.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/SuperAdminPanel.jsx +167 -17
frontend/src/pages/SuperAdminPanel.jsx CHANGED
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
2
  import { Link } from 'react-router-dom';
3
  import { useAuth } from '../components/AuthContext';
4
  import { CustomModal } from '../components/CustomModal';
5
- import { Building2, Users, CreditCard, Shield, Trash2, Plus, Server, Activity, Database, HardDrive, RefreshCw, BarChart2, Edit, Download, Eye } from 'lucide-react';
6
  import toast from 'react-hot-toast';
7
 
8
  const DEFAULT_BILLING_TIERS = [
@@ -307,6 +307,11 @@ const SuperAdminPanel = () => {
307
  const [sortUserCol, setSortUserCol] = useState('Email');
308
  const [sortUserDir, setSortUserDir] = useState('asc');
309
 
 
 
 
 
 
310
  const [sortBillCol, setSortBillCol] = useState('Date');
311
  const [sortBillDir, setSortBillDir] = useState('desc');
312
 
@@ -361,8 +366,35 @@ const SuperAdminPanel = () => {
361
  }
362
  };
363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  const getSortedUsers = () => {
365
- return [...(users || [])].sort((a, b) => {
 
366
  let aVal, bVal;
367
  switch (sortUserCol) {
368
  case 'Email': aVal = a.email || ''; bVal = b.email || ''; break;
@@ -376,6 +408,13 @@ const SuperAdminPanel = () => {
376
  });
377
  };
378
 
 
 
 
 
 
 
 
379
  const handleBillSort = (column) => {
380
  if (column === 'Invoice') return;
381
  if (sortBillCol === column) {
@@ -810,17 +849,21 @@ const SuperAdminPanel = () => {
810
  { key: 'org_id', label: 'Organization', type: 'select', options: orgOptions }
811
  ],
812
  onConfirm: async (values) => {
 
 
 
 
813
  try {
814
- const res = await fetch('/api/auth/users', {
815
  method: 'POST',
816
- headers: { 'Authorization': `Bearer ${localStorage.getItem('wss_token')}`, 'Content-Type': 'application/json' },
817
- body: JSON.stringify({ email: values.email, role: values.role, org_id: values.org_id || null })
818
  });
 
819
  if (res.ok) {
820
  toast.success('Member added successfully');
821
  fetchStats();
822
  } else {
823
- const data = await res.json();
824
  toast.error(data.message || 'Failed to add member');
825
  }
826
  } catch (err) {
@@ -860,9 +903,9 @@ const SuperAdminPanel = () => {
860
  ],
861
  onConfirm: async (vals) => {
862
  try {
863
- const res = await fetch(`/api/auth/users/${u.id}/role`, {
864
  method: 'PUT',
865
- headers: { 'Authorization': `Bearer ${localStorage.getItem('wss_token')}`, 'Content-Type': 'application/json' },
866
  body: JSON.stringify({ role: vals.role, org_id: vals.org_id || null })
867
  });
868
  if (res.ok) {
@@ -1382,14 +1425,115 @@ const SuperAdminPanel = () => {
1382
 
1383
  {activeTab === 'members' && (
1384
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
1385
- <div className="flex justify-between items-center mb-md">
1386
- <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px]"><Users className="w-5 h-5 text-primary mr-2" /> Global Members</h2>
 
 
 
 
 
 
 
1387
  {!isSupportEngineer && (
1388
- <button onClick={handleAddMember} className="flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:brightness-110 transition-all font-bold text-[13.5px] border-0 cursor-pointer"><Plus className="w-4 h-4 mr-2" /> Add Member</button>
 
 
1389
  )}
1390
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1391
  <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto hide-scrollbar">
1392
- {loading ? <div className="p-10 text-center text-on-surface-variant">Fetching users...</div> : (
 
 
 
 
 
 
 
 
 
 
 
1393
  <table className="w-full text-left text-sm border-collapse">
1394
  <thead className="bg-surface-container text-on-surface-variant border-b border-outline-variant select-none">
1395
  <tr>
@@ -1429,12 +1573,18 @@ const SuperAdminPanel = () => {
1429
  type: 'error',
1430
  onConfirm: async () => {
1431
  try {
1432
- await fetch(`/api/auth/users/${u.id}`, {
1433
- method: 'DELETE',
1434
- headers: { 'Authorization': `Bearer ${localStorage.getItem('wss_token')}` }
1435
  });
1436
- fetchStats();
1437
- } catch (err) { }
 
 
 
 
 
 
 
1438
  closeConfirm();
1439
  }
1440
  });
 
2
  import { Link } from 'react-router-dom';
3
  import { useAuth } from '../components/AuthContext';
4
  import { CustomModal } from '../components/CustomModal';
5
+ import { Building2, Users, CreditCard, Shield, Trash2, Plus, Server, Activity, Database, HardDrive, RefreshCw, BarChart2, Edit, Download, Eye, Search, X } from 'lucide-react';
6
  import toast from 'react-hot-toast';
7
 
8
  const DEFAULT_BILLING_TIERS = [
 
307
  const [sortUserCol, setSortUserCol] = useState('Email');
308
  const [sortUserDir, setSortUserDir] = useState('asc');
309
 
310
+ // Filter states for Global Members
311
+ const [userSearch, setUserSearch] = useState('');
312
+ const [userRoleFilter, setUserRoleFilter] = useState('all');
313
+ const [userOrgFilter, setUserOrgFilter] = useState('all');
314
+
315
  const [sortBillCol, setSortBillCol] = useState('Date');
316
  const [sortBillDir, setSortBillDir] = useState('desc');
317
 
 
366
  }
367
  };
368
 
369
+ const getFilteredUsers = () => {
370
+ return (users || []).filter(u => {
371
+ if (userSearch) {
372
+ const q = userSearch.toLowerCase().trim();
373
+ const emailMatch = (u.email || '').toLowerCase().includes(q);
374
+ const roleMatch = (u.role || '').toLowerCase().includes(q);
375
+ const orgMatch = (u.org_name || '').toLowerCase().includes(q);
376
+ if (!emailMatch && !roleMatch && !orgMatch) return false;
377
+ }
378
+
379
+ if (userRoleFilter !== 'all') {
380
+ if ((u.role || '').toLowerCase() !== userRoleFilter.toLowerCase()) return false;
381
+ }
382
+
383
+ if (userOrgFilter !== 'all') {
384
+ if (userOrgFilter === 'no_org') {
385
+ if (u.org_id || (u.org_name && u.org_name !== 'No Org (Super Admin)')) return false;
386
+ } else {
387
+ if (String(u.org_id) !== String(userOrgFilter) && u.org_name !== userOrgFilter) return false;
388
+ }
389
+ }
390
+
391
+ return true;
392
+ });
393
+ };
394
+
395
  const getSortedUsers = () => {
396
+ const filtered = getFilteredUsers();
397
+ return [...filtered].sort((a, b) => {
398
  let aVal, bVal;
399
  switch (sortUserCol) {
400
  case 'Email': aVal = a.email || ''; bVal = b.email || ''; break;
 
408
  });
409
  };
410
 
411
+ const clearUserFilters = () => {
412
+ setUserSearch('');
413
+ setUserRoleFilter('all');
414
+ setUserOrgFilter('all');
415
+ setUserPage(1);
416
+ };
417
+
418
  const handleBillSort = (column) => {
419
  if (column === 'Invoice') return;
420
  if (sortBillCol === column) {
 
849
  { key: 'org_id', label: 'Organization', type: 'select', options: orgOptions }
850
  ],
851
  onConfirm: async (values) => {
852
+ if (!values.email || !values.email.trim()) {
853
+ toast.error('Please enter a valid user email');
854
+ return;
855
+ }
856
  try {
857
+ const res = await authFetch('/api/auth/users', {
858
  method: 'POST',
859
+ headers: { 'Content-Type': 'application/json' },
860
+ body: JSON.stringify({ email: values.email.trim(), role: values.role, org_id: values.org_id || null })
861
  });
862
+ const data = await res.json().catch(() => ({}));
863
  if (res.ok) {
864
  toast.success('Member added successfully');
865
  fetchStats();
866
  } else {
 
867
  toast.error(data.message || 'Failed to add member');
868
  }
869
  } catch (err) {
 
903
  ],
904
  onConfirm: async (vals) => {
905
  try {
906
+ const res = await authFetch(`/api/auth/users/${u.id}/role`, {
907
  method: 'PUT',
908
+ headers: { 'Content-Type': 'application/json' },
909
  body: JSON.stringify({ role: vals.role, org_id: vals.org_id || null })
910
  });
911
  if (res.ok) {
 
1425
 
1426
  {activeTab === 'members' && (
1427
  <div className="flex flex-col gap-md mb-xl animate-fade-in">
1428
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-md mb-xs">
1429
+ <div>
1430
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px]">
1431
+ <Users className="w-5 h-5 text-primary mr-2" /> Global Members
1432
+ </h2>
1433
+ <p className="text-[13px] text-on-surface-variant mt-0.5">
1434
+ Centralized user management, role assignments, and client org mapping.
1435
+ </p>
1436
+ </div>
1437
  {!isSupportEngineer && (
1438
+ <button onClick={handleAddMember} className="flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:brightness-110 transition-all font-bold text-[13.5px] border-0 cursor-pointer shadow-md shadow-primary/20 shrink-0">
1439
+ <Plus className="w-4 h-4 mr-2" /> Add Member
1440
+ </button>
1441
  )}
1442
  </div>
1443
+
1444
+ {/* Filter & Search Bar */}
1445
+ <div className="bg-surface-container-lowest border border-outline-variant/70 rounded-2xl p-4 flex flex-col md:flex-row items-center justify-between gap-3 shadow-2xs">
1446
+ <div className="flex flex-wrap items-center gap-3 w-full flex-1">
1447
+ {/* Search Input */}
1448
+ <div className="relative w-full sm:w-72">
1449
+ <Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant" />
1450
+ <input
1451
+ type="text"
1452
+ placeholder="Search email, org, role..."
1453
+ value={userSearch}
1454
+ onChange={(e) => {
1455
+ setUserSearch(e.target.value);
1456
+ setUserPage(1);
1457
+ }}
1458
+ className="w-full pl-9 pr-8 py-2 bg-surface border border-outline-variant/60 rounded-lg text-[13px] text-on-surface focus:outline-none focus:border-primary"
1459
+ />
1460
+ {userSearch && (
1461
+ <button
1462
+ onClick={() => { setUserSearch(''); setUserPage(1); }}
1463
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 text-on-surface-variant hover:text-on-surface bg-transparent border-0 cursor-pointer p-0.5"
1464
+ >
1465
+ <X className="w-3.5 h-3.5" />
1466
+ </button>
1467
+ )}
1468
+ </div>
1469
+
1470
+ {/* Filter by Role */}
1471
+ <div className="flex items-center gap-1.5 w-full sm:w-auto">
1472
+ <span className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider shrink-0">Role:</span>
1473
+ <select
1474
+ value={userRoleFilter}
1475
+ onChange={(e) => {
1476
+ setUserRoleFilter(e.target.value);
1477
+ setUserPage(1);
1478
+ }}
1479
+ className="bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg px-3 py-2 focus:outline-none focus:border-primary cursor-pointer w-full sm:w-auto"
1480
+ >
1481
+ <option value="all">All Roles</option>
1482
+ <option value="admin">Admin</option>
1483
+ <option value="soc_analyst">SOC Analyst</option>
1484
+ <option value="org_admin">Organization Admin</option>
1485
+ <option value="executive_user">Executive User</option>
1486
+ <option value="super_admin">Super Admin</option>
1487
+ <option value="support_engineer">Support Engineer</option>
1488
+ <option value="read_only">Read Only</option>
1489
+ </select>
1490
+ </div>
1491
+
1492
+ {/* Filter by Organization */}
1493
+ <div className="flex items-center gap-1.5 w-full sm:w-auto">
1494
+ <span className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider shrink-0">Organization:</span>
1495
+ <select
1496
+ value={userOrgFilter}
1497
+ onChange={(e) => {
1498
+ setUserOrgFilter(e.target.value);
1499
+ setUserPage(1);
1500
+ }}
1501
+ className="bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg px-3 py-2 focus:outline-none focus:border-primary cursor-pointer w-full sm:w-auto max-w-[200px] truncate"
1502
+ >
1503
+ <option value="all">All Organizations</option>
1504
+ <option value="no_org">No Org (Super Admin / Global)</option>
1505
+ {(organizations || []).map(org => (
1506
+ <option key={org.id} value={org.id}>{org.name}</option>
1507
+ ))}
1508
+ </select>
1509
+ </div>
1510
+
1511
+ {/* Reset Button */}
1512
+ {(userSearch || userRoleFilter !== 'all' || userOrgFilter !== 'all') && (
1513
+ <button
1514
+ onClick={clearUserFilters}
1515
+ className="text-error hover:underline text-[12.5px] font-bold flex items-center gap-1 cursor-pointer bg-transparent border-0 px-1 ml-auto"
1516
+ >
1517
+ <X className="w-4 h-4" />
1518
+ Reset Filters
1519
+ </button>
1520
+ )}
1521
+ </div>
1522
+ </div>
1523
+
1524
  <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto hide-scrollbar">
1525
+ {loading ? <div className="p-10 text-center text-on-surface-variant">Fetching users...</div> : getSortedUsers().length === 0 ? (
1526
+ <div className="p-12 text-center text-on-surface-variant">
1527
+ <Users className="w-8 h-8 text-on-surface-variant/40 mx-auto mb-2" />
1528
+ <p className="font-bold text-[14px] text-on-surface">No members match your filter criteria.</p>
1529
+ <p className="text-[12.5px] text-on-surface-variant mt-1">Try clearing your search terms or filter criteria.</p>
1530
+ {(userSearch || userRoleFilter !== 'all' || userOrgFilter !== 'all') && (
1531
+ <button onClick={clearUserFilters} className="mt-3 px-3.5 py-1.5 bg-primary/10 text-primary font-bold text-[12px] rounded-lg border border-primary/20 hover:bg-primary/20 cursor-pointer">
1532
+ Clear All Filters
1533
+ </button>
1534
+ )}
1535
+ </div>
1536
+ ) : (
1537
  <table className="w-full text-left text-sm border-collapse">
1538
  <thead className="bg-surface-container text-on-surface-variant border-b border-outline-variant select-none">
1539
  <tr>
 
1573
  type: 'error',
1574
  onConfirm: async () => {
1575
  try {
1576
+ const res = await authFetch(`/api/auth/users/${u.id}`, {
1577
+ method: 'DELETE'
 
1578
  });
1579
+ if (res.ok) {
1580
+ toast.success('Member deleted successfully');
1581
+ fetchStats();
1582
+ } else {
1583
+ toast.error('Failed to delete member');
1584
+ }
1585
+ } catch (err) {
1586
+ toast.error('Network error deleting member');
1587
+ }
1588
  closeConfirm();
1589
  }
1590
  });