Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update frontend/src/pages/SupportEngineerPanel.jsx
Browse files
frontend/src/pages/SupportEngineerPanel.jsx
CHANGED
|
@@ -17,7 +17,15 @@ import {
|
|
| 17 |
RefreshCw,
|
| 18 |
CheckCircle2,
|
| 19 |
AlertTriangle,
|
| 20 |
-
Info
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
} from 'lucide-react';
|
| 22 |
import { CustomModal } from '../components/CustomModal';
|
| 23 |
|
|
@@ -130,6 +138,7 @@ export const SupportEngineerPanel = () => {
|
|
| 130 |
const [metrics, setMetrics] = useState({});
|
| 131 |
const [auditLogs, setAuditLogs] = useState([]);
|
| 132 |
const [users, setUsers] = useState([]);
|
|
|
|
| 133 |
const [demoBookings, setDemoBookings] = useState([]);
|
| 134 |
const [emailLogs, setEmailLogs] = useState([]);
|
| 135 |
const [loading, setLoading] = useState(true);
|
|
@@ -137,12 +146,16 @@ export const SupportEngineerPanel = () => {
|
|
| 137 |
const [activeTab, setActiveTab] = useState(() => {
|
| 138 |
try {
|
| 139 |
const params = new URLSearchParams(window.location.search);
|
| 140 |
-
return params.get('tab') || '
|
| 141 |
} catch (e) {
|
| 142 |
-
return '
|
| 143 |
}
|
| 144 |
});
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
// Table sorting states
|
| 147 |
const [sortOrgCol, setSortOrgCol] = useState('Tenant Name');
|
| 148 |
const [sortOrgDir, setSortOrgDir] = useState('asc');
|
|
@@ -171,13 +184,19 @@ export const SupportEngineerPanel = () => {
|
|
| 171 |
const [emailPage, setEmailPage] = useState(1);
|
| 172 |
const [emailPageSize, setEmailPageSize] = useState(25);
|
| 173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
// Custom Prompt Modal state for editing member roles/orgs (without password)
|
| 175 |
const [promptModal, setPromptModal] = useState({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null });
|
| 176 |
const [promptValues, setPromptValues] = useState({});
|
| 177 |
-
const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', desc: '', onConfirm: null });
|
| 178 |
|
| 179 |
const closePrompt = () => { setPromptModal({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null }); setPromptValues({}); };
|
| 180 |
-
const closeConfirm = () => setConfirmModal({ isOpen: false, title: '', desc: '', onConfirm: null });
|
| 181 |
|
| 182 |
const handlePromptChange = (key, value) => setPromptValues(prev => ({ ...prev, [key]: value }));
|
| 183 |
|
|
@@ -199,6 +218,7 @@ export const SupportEngineerPanel = () => {
|
|
| 199 |
const data = await globalRes.json();
|
| 200 |
setOrganizations(Array.isArray(data.organizations) ? data.organizations : []);
|
| 201 |
setMetrics(data.metrics || {});
|
|
|
|
| 202 |
setAuditLogs(Array.isArray(data.audit_logs) ? data.audit_logs : []);
|
| 203 |
setUsers(Array.isArray(data.users) ? data.users : []);
|
| 204 |
}
|
|
@@ -221,6 +241,103 @@ export const SupportEngineerPanel = () => {
|
|
| 221 |
fetchStats();
|
| 222 |
}, []);
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
const handleOrgSort = (column) => {
|
| 225 |
if (column === 'Quotas' || column === 'Actions') return;
|
| 226 |
if (sortOrgCol === column) {
|
|
@@ -295,6 +412,7 @@ export const SupportEngineerPanel = () => {
|
|
| 295 |
isOpen: true,
|
| 296 |
title: 'Inspect Tenant Environment',
|
| 297 |
desc: `Log in as administrator for ${orgName} to troubleshoot customer issue?`,
|
|
|
|
| 298 |
onConfirm: async () => {
|
| 299 |
try {
|
| 300 |
const res = await authFetch(`/api/auth/impersonate/${orgId}`, {
|
|
@@ -316,7 +434,6 @@ export const SupportEngineerPanel = () => {
|
|
| 316 |
});
|
| 317 |
};
|
| 318 |
|
| 319 |
-
// Support Engineer can edit role / org assignment, BUT NOT passwords
|
| 320 |
const handleEditMember = (u) => {
|
| 321 |
const orgOptions = [
|
| 322 |
{ label: 'None (Global)', value: '' },
|
|
@@ -446,62 +563,214 @@ export const SupportEngineerPanel = () => {
|
|
| 446 |
<div className="flex flex-col gap-lg animate-fade-in">
|
| 447 |
{/* Quick Metrics Grid */}
|
| 448 |
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-md">
|
| 449 |
-
<div className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl flex items-center justify-between shadow-2xs">
|
| 450 |
<div>
|
| 451 |
<div className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wider">Total Tenants</div>
|
| 452 |
<div className="text-2xl font-extrabold text-on-surface mt-1">{metrics.total_tenants || organizations.length || 0}</div>
|
| 453 |
</div>
|
| 454 |
-
<div className="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center">
|
| 455 |
-
<
|
| 456 |
</div>
|
| 457 |
</div>
|
| 458 |
|
| 459 |
-
<div className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl flex items-center justify-between shadow-2xs">
|
| 460 |
<div>
|
| 461 |
<div className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wider">Active Licenses</div>
|
| 462 |
<div className="text-2xl font-extrabold text-on-surface mt-1">{metrics.active_licenses || 0}</div>
|
| 463 |
</div>
|
| 464 |
-
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-600 flex items-center justify-center">
|
| 465 |
-
<
|
| 466 |
</div>
|
| 467 |
</div>
|
| 468 |
|
| 469 |
-
<div className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl flex items-center justify-between shadow-2xs">
|
| 470 |
<div>
|
| 471 |
<div className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wider">Global Users</div>
|
| 472 |
<div className="text-2xl font-extrabold text-on-surface mt-1">{metrics.global_users || users.length || 0}</div>
|
| 473 |
</div>
|
| 474 |
-
<div className="w-10 h-10 rounded-xl bg-purple-500/10 text-purple-600 flex items-center justify-center">
|
| 475 |
<Users className="w-5 h-5" />
|
| 476 |
</div>
|
| 477 |
</div>
|
| 478 |
|
| 479 |
-
<div className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl flex items-center justify-between shadow-2xs">
|
| 480 |
<div>
|
| 481 |
<div className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wider">Active Scanners</div>
|
| 482 |
-
<div className="text-2xl font-extrabold text-on-surface mt-1">{metrics.active_scanners || 0}</div>
|
| 483 |
</div>
|
| 484 |
-
<div className="w-10 h-10 rounded-xl bg-blue-500/10 text-blue-600 flex items-center justify-center">
|
| 485 |
-
<
|
| 486 |
</div>
|
| 487 |
</div>
|
| 488 |
</div>
|
| 489 |
|
| 490 |
-
{/*
|
| 491 |
-
<div className="
|
| 492 |
-
<div className="
|
| 493 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
</div>
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
<
|
| 503 |
-
<
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
</div>
|
| 506 |
</div>
|
| 507 |
</div>
|
|
@@ -865,6 +1134,89 @@ export const SupportEngineerPanel = () => {
|
|
| 865 |
</div>
|
| 866 |
)}
|
| 867 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 868 |
{/* Prompt Modal for Editing Role/Org */}
|
| 869 |
<CustomModal
|
| 870 |
isOpen={promptModal.isOpen}
|
|
@@ -918,7 +1270,7 @@ export const SupportEngineerPanel = () => {
|
|
| 918 |
footer={
|
| 919 |
<>
|
| 920 |
<button onClick={closeConfirm} className="px-4 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg font-bold border-0 bg-transparent cursor-pointer">Cancel</button>
|
| 921 |
-
<button onClick={confirmModal.onConfirm} className=
|
| 922 |
</>
|
| 923 |
}
|
| 924 |
/>
|
|
|
|
| 17 |
RefreshCw,
|
| 18 |
CheckCircle2,
|
| 19 |
AlertTriangle,
|
| 20 |
+
Info,
|
| 21 |
+
Building2,
|
| 22 |
+
CreditCard,
|
| 23 |
+
Server,
|
| 24 |
+
Database,
|
| 25 |
+
HardDrive,
|
| 26 |
+
Eye,
|
| 27 |
+
Download,
|
| 28 |
+
Receipt
|
| 29 |
} from 'lucide-react';
|
| 30 |
import { CustomModal } from '../components/CustomModal';
|
| 31 |
|
|
|
|
| 138 |
const [metrics, setMetrics] = useState({});
|
| 139 |
const [auditLogs, setAuditLogs] = useState([]);
|
| 140 |
const [users, setUsers] = useState([]);
|
| 141 |
+
const [recentPayments, setRecentPayments] = useState([]);
|
| 142 |
const [demoBookings, setDemoBookings] = useState([]);
|
| 143 |
const [emailLogs, setEmailLogs] = useState([]);
|
| 144 |
const [loading, setLoading] = useState(true);
|
|
|
|
| 146 |
const [activeTab, setActiveTab] = useState(() => {
|
| 147 |
try {
|
| 148 |
const params = new URLSearchParams(window.location.search);
|
| 149 |
+
return params.get('tab') || 'overview';
|
| 150 |
} catch (e) {
|
| 151 |
+
return 'overview';
|
| 152 |
}
|
| 153 |
});
|
| 154 |
|
| 155 |
+
// Billing Sorting State
|
| 156 |
+
const [sortBillCol, setSortBillCol] = useState('Date');
|
| 157 |
+
const [sortBillDir, setSortBillDir] = useState('desc');
|
| 158 |
+
|
| 159 |
// Table sorting states
|
| 160 |
const [sortOrgCol, setSortOrgCol] = useState('Tenant Name');
|
| 161 |
const [sortOrgDir, setSortOrgDir] = useState('asc');
|
|
|
|
| 184 |
const [emailPage, setEmailPage] = useState(1);
|
| 185 |
const [emailPageSize, setEmailPageSize] = useState(25);
|
| 186 |
|
| 187 |
+
// Active Scans & Invoice Modals
|
| 188 |
+
const [activeScansModalOpen, setActiveScansModalOpen] = useState(false);
|
| 189 |
+
const [activeScans, setActiveScans] = useState([]);
|
| 190 |
+
const [fetchingActiveScans, setFetchingActiveScans] = useState(false);
|
| 191 |
+
const [viewInvoice, setViewInvoice] = useState(null);
|
| 192 |
+
|
| 193 |
// Custom Prompt Modal state for editing member roles/orgs (without password)
|
| 194 |
const [promptModal, setPromptModal] = useState({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null });
|
| 195 |
const [promptValues, setPromptValues] = useState({});
|
| 196 |
+
const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', desc: '', onConfirm: null, type: 'primary' });
|
| 197 |
|
| 198 |
const closePrompt = () => { setPromptModal({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null }); setPromptValues({}); };
|
| 199 |
+
const closeConfirm = () => setConfirmModal({ isOpen: false, title: '', desc: '', onConfirm: null, type: 'primary' });
|
| 200 |
|
| 201 |
const handlePromptChange = (key, value) => setPromptValues(prev => ({ ...prev, [key]: value }));
|
| 202 |
|
|
|
|
| 218 |
const data = await globalRes.json();
|
| 219 |
setOrganizations(Array.isArray(data.organizations) ? data.organizations : []);
|
| 220 |
setMetrics(data.metrics || {});
|
| 221 |
+
setRecentPayments(Array.isArray(data.recent_payments) ? data.recent_payments : []);
|
| 222 |
setAuditLogs(Array.isArray(data.audit_logs) ? data.audit_logs : []);
|
| 223 |
setUsers(Array.isArray(data.users) ? data.users : []);
|
| 224 |
}
|
|
|
|
| 241 |
fetchStats();
|
| 242 |
}, []);
|
| 243 |
|
| 244 |
+
const openActiveScansModal = async () => {
|
| 245 |
+
setActiveScansModalOpen(true);
|
| 246 |
+
setFetchingActiveScans(true);
|
| 247 |
+
try {
|
| 248 |
+
const res = await authFetch('/api/scans/active');
|
| 249 |
+
if (res.ok) {
|
| 250 |
+
const data = await res.json();
|
| 251 |
+
setActiveScans(data.scans || []);
|
| 252 |
+
}
|
| 253 |
+
} catch (err) {
|
| 254 |
+
console.error("Error fetching active scans:", err);
|
| 255 |
+
}
|
| 256 |
+
setFetchingActiveScans(false);
|
| 257 |
+
};
|
| 258 |
+
|
| 259 |
+
const handleKillScan = (scanId) => {
|
| 260 |
+
setConfirmModal({
|
| 261 |
+
isOpen: true,
|
| 262 |
+
title: 'Terminate Scan',
|
| 263 |
+
desc: 'Are you sure you want to forcibly terminate this scan?',
|
| 264 |
+
type: 'error',
|
| 265 |
+
onConfirm: async () => {
|
| 266 |
+
try {
|
| 267 |
+
await authFetch(`/api/scans/${scanId}/terminate`, { method: 'POST' });
|
| 268 |
+
toast.success('Scan terminated successfully');
|
| 269 |
+
openActiveScansModal();
|
| 270 |
+
} catch (err) {
|
| 271 |
+
toast.error('Failed to terminate scan');
|
| 272 |
+
}
|
| 273 |
+
closeConfirm();
|
| 274 |
+
}
|
| 275 |
+
});
|
| 276 |
+
};
|
| 277 |
+
|
| 278 |
+
const handleBillSort = (column) => {
|
| 279 |
+
if (column === 'Invoice') return;
|
| 280 |
+
if (sortBillCol === column) {
|
| 281 |
+
setSortBillDir(sortBillDir === 'asc' ? 'desc' : 'asc');
|
| 282 |
+
} else {
|
| 283 |
+
setSortBillCol(column);
|
| 284 |
+
setSortBillDir('asc');
|
| 285 |
+
}
|
| 286 |
+
};
|
| 287 |
+
|
| 288 |
+
const getSortedBills = () => {
|
| 289 |
+
return [...(recentPayments || [])].sort((a, b) => {
|
| 290 |
+
let aVal, bVal;
|
| 291 |
+
switch (sortBillCol) {
|
| 292 |
+
case 'Date': aVal = new Date(a.created_at).getTime(); bVal = new Date(b.created_at).getTime(); break;
|
| 293 |
+
case 'Organization / User': aVal = a.org_name || ''; bVal = b.org_name || ''; break;
|
| 294 |
+
case 'Tier': aVal = a.tier_id || ''; bVal = b.tier_id || ''; break;
|
| 295 |
+
case 'Amount': aVal = a.amount || 0; bVal = b.amount || 0; break;
|
| 296 |
+
case 'Status': aVal = a.status || ''; bVal = b.status || ''; break;
|
| 297 |
+
default: aVal = new Date(a.created_at).getTime(); bVal = new Date(b.created_at).getTime();
|
| 298 |
+
}
|
| 299 |
+
if (aVal < bVal) return sortBillDir === 'asc' ? -1 : 1;
|
| 300 |
+
if (aVal > bVal) return sortBillDir === 'asc' ? 1 : -1;
|
| 301 |
+
return 0;
|
| 302 |
+
});
|
| 303 |
+
};
|
| 304 |
+
|
| 305 |
+
const handleDownloadInvoice = (payment) => {
|
| 306 |
+
const invoiceHtml = `
|
| 307 |
+
<html>
|
| 308 |
+
<head>
|
| 309 |
+
<title>Invoice - ${payment.id}</title>
|
| 310 |
+
<style>
|
| 311 |
+
body { font-family: sans-serif; padding: 40px; color: #333; }
|
| 312 |
+
h1 { color: #2563eb; margin-bottom: 5px; }
|
| 313 |
+
.header { border-bottom: 2px solid #eee; padding-bottom: 20px; margin-bottom: 20px; }
|
| 314 |
+
.row { display: flex; justify-content: space-between; margin-bottom: 10px; font-size: 14px; }
|
| 315 |
+
.total { font-size: 20px; font-weight: bold; margin-top: 30px; border-top: 2px solid #eee; padding-top: 20px; }
|
| 316 |
+
</style>
|
| 317 |
+
</head>
|
| 318 |
+
<body>
|
| 319 |
+
<div class="header">
|
| 320 |
+
<h1>LarShield</h1>
|
| 321 |
+
<p style="margin: 0; color: #666;">Payment Receipt & Invoice</p>
|
| 322 |
+
</div>
|
| 323 |
+
<div class="row"><strong>Date:</strong> <span>${new Date(payment.created_at).toLocaleString()}</span></div>
|
| 324 |
+
<div class="row"><strong>Organization:</strong> <span>${payment.org_name}</span></div>
|
| 325 |
+
<div class="row"><strong>Email:</strong> <span>${payment.user_email}</span></div>
|
| 326 |
+
<div class="row"><strong>Subscription Tier:</strong> <span><span style="text-transform: capitalize;">${payment.tier_id}</span> Plan</span></div>
|
| 327 |
+
<div class="row"><strong>Status:</strong> <span style="color: ${payment.status === 'successful' ? 'green' : 'red'}">${payment.status.toUpperCase()}</span></div>
|
| 328 |
+
<div class="row total">
|
| 329 |
+
<strong>Total Amount:</strong>
|
| 330 |
+
<span>${payment.currency === 'INR' ? '₹' : '$'}${payment.amount.toLocaleString(undefined, { minimumFractionDigits: 2 })}</span>
|
| 331 |
+
</div>
|
| 332 |
+
<script>window.print();</script>
|
| 333 |
+
</body>
|
| 334 |
+
</html>
|
| 335 |
+
`;
|
| 336 |
+
const blob = new Blob([invoiceHtml], { type: 'text/html' });
|
| 337 |
+
const url = URL.createObjectURL(blob);
|
| 338 |
+
window.open(url, '_blank');
|
| 339 |
+
};
|
| 340 |
+
|
| 341 |
const handleOrgSort = (column) => {
|
| 342 |
if (column === 'Quotas' || column === 'Actions') return;
|
| 343 |
if (sortOrgCol === column) {
|
|
|
|
| 412 |
isOpen: true,
|
| 413 |
title: 'Inspect Tenant Environment',
|
| 414 |
desc: `Log in as administrator for ${orgName} to troubleshoot customer issue?`,
|
| 415 |
+
type: 'primary',
|
| 416 |
onConfirm: async () => {
|
| 417 |
try {
|
| 418 |
const res = await authFetch(`/api/auth/impersonate/${orgId}`, {
|
|
|
|
| 434 |
});
|
| 435 |
};
|
| 436 |
|
|
|
|
| 437 |
const handleEditMember = (u) => {
|
| 438 |
const orgOptions = [
|
| 439 |
{ label: 'None (Global)', value: '' },
|
|
|
|
| 563 |
<div className="flex flex-col gap-lg animate-fade-in">
|
| 564 |
{/* Quick Metrics Grid */}
|
| 565 |
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-md">
|
| 566 |
+
<div className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl flex items-center justify-between shadow-2xs hover:shadow-md transition-all group">
|
| 567 |
<div>
|
| 568 |
<div className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wider">Total Tenants</div>
|
| 569 |
<div className="text-2xl font-extrabold text-on-surface mt-1">{metrics.total_tenants || organizations.length || 0}</div>
|
| 570 |
</div>
|
| 571 |
+
<div className="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center group-hover:scale-110 transition-transform">
|
| 572 |
+
<Building2 className="w-5 h-5" />
|
| 573 |
</div>
|
| 574 |
</div>
|
| 575 |
|
| 576 |
+
<div className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl flex items-center justify-between shadow-2xs hover:shadow-md transition-all group">
|
| 577 |
<div>
|
| 578 |
<div className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wider">Active Licenses</div>
|
| 579 |
<div className="text-2xl font-extrabold text-on-surface mt-1">{metrics.active_licenses || 0}</div>
|
| 580 |
</div>
|
| 581 |
+
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-600 flex items-center justify-center group-hover:scale-110 transition-transform">
|
| 582 |
+
<CreditCard className="w-5 h-5" />
|
| 583 |
</div>
|
| 584 |
</div>
|
| 585 |
|
| 586 |
+
<div className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl flex items-center justify-between shadow-2xs hover:shadow-md transition-all group">
|
| 587 |
<div>
|
| 588 |
<div className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wider">Global Users</div>
|
| 589 |
<div className="text-2xl font-extrabold text-on-surface mt-1">{metrics.global_users || users.length || 0}</div>
|
| 590 |
</div>
|
| 591 |
+
<div className="w-10 h-10 rounded-xl bg-purple-500/10 text-purple-600 flex items-center justify-center group-hover:scale-110 transition-transform">
|
| 592 |
<Users className="w-5 h-5" />
|
| 593 |
</div>
|
| 594 |
</div>
|
| 595 |
|
| 596 |
+
<div className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl flex items-center justify-between shadow-2xs hover:shadow-md transition-all group">
|
| 597 |
<div>
|
| 598 |
<div className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wider">Active Scanners</div>
|
| 599 |
+
<div className="text-2xl font-extrabold text-on-surface mt-1">{metrics.active_scanners || 0}<span className="text-sm text-on-surface-variant font-medium">/{metrics.total_scanners || 50}</span></div>
|
| 600 |
</div>
|
| 601 |
+
<div className="w-10 h-10 rounded-xl bg-blue-500/10 text-blue-600 flex items-center justify-center group-hover:scale-110 transition-transform">
|
| 602 |
+
<Server className="w-5 h-5" />
|
| 603 |
</div>
|
| 604 |
</div>
|
| 605 |
</div>
|
| 606 |
|
| 607 |
+
{/* Infrastructure Node & System Health Cards */}
|
| 608 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-md">
|
| 609 |
+
<div className="flex flex-col gap-md">
|
| 610 |
+
<h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[16.5px]">
|
| 611 |
+
<Activity className="w-4 h-4 text-primary mr-2" /> Node Infrastructure & Health
|
| 612 |
+
</h2>
|
| 613 |
+
|
| 614 |
+
<div className="bg-surface-container-lowest border border-outline-variant rounded-2xl p-md flex flex-col gap-sm shadow-2xs">
|
| 615 |
+
<div className="flex justify-between items-center pb-xs border-b border-outline-variant/50">
|
| 616 |
+
<div className="flex items-center gap-2 text-[13px] font-bold text-on-surface">
|
| 617 |
+
<Database className="w-4 h-4 text-primary" /> Database Cluster Connection Load
|
| 618 |
+
</div>
|
| 619 |
+
<span className="text-[11px] font-bold bg-green-500/10 text-green-600 px-2 py-0.5 rounded border border-green-500/20">
|
| 620 |
+
HEALTHY
|
| 621 |
+
</span>
|
| 622 |
+
</div>
|
| 623 |
+
<div className="flex justify-between items-center text-[12.5px]">
|
| 624 |
+
<span className="text-on-surface-variant font-semibold">Active Connections</span>
|
| 625 |
+
<span className="font-bold text-on-surface">{metrics.db_connections || 14} / 500</span>
|
| 626 |
+
</div>
|
| 627 |
+
</div>
|
| 628 |
+
|
| 629 |
+
<div className="bg-surface-container-lowest border border-outline-variant rounded-2xl p-md flex flex-col gap-sm shadow-2xs">
|
| 630 |
+
<div className="flex justify-between items-center pb-xs border-b border-outline-variant/50">
|
| 631 |
+
<div className="flex items-center gap-2 text-[13px] font-bold text-on-surface">
|
| 632 |
+
<HardDrive className="w-4 h-4 text-primary" /> Celery Workers & Queue Status
|
| 633 |
+
</div>
|
| 634 |
+
<span className={`text-[11px] font-bold px-2 py-0.5 rounded border ${metrics.queue_size > 5 ? 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20' : 'bg-green-500/10 text-green-600 border-green-500/20'}`}>
|
| 635 |
+
{metrics.queue_size > 5 ? 'HEAVY LOAD' : 'NORMAL'}
|
| 636 |
+
</span>
|
| 637 |
+
</div>
|
| 638 |
+
<div className="flex justify-between items-center text-[12.5px]">
|
| 639 |
+
<span className="text-on-surface-variant font-semibold">Queue Size</span>
|
| 640 |
+
<span className="font-bold text-on-surface">{metrics.queue_size || 0} scans pending</span>
|
| 641 |
+
</div>
|
| 642 |
+
</div>
|
| 643 |
+
|
| 644 |
+
<button
|
| 645 |
+
onClick={openActiveScansModal}
|
| 646 |
+
className="w-full bg-surface-container border border-outline-variant text-primary py-2.5 rounded-xl text-[13px] font-bold hover:bg-primary/10 hover:border-primary/30 cursor-pointer transition-colors flex items-center justify-center gap-2 shadow-2xs"
|
| 647 |
+
>
|
| 648 |
+
<span className="material-symbols-outlined text-[18px]">screen_search</span>
|
| 649 |
+
Inspect Active Scans
|
| 650 |
+
</button>
|
| 651 |
</div>
|
| 652 |
+
|
| 653 |
+
{/* Support Guidance Box */}
|
| 654 |
+
<div className="bg-blue-500/5 border border-blue-500/20 rounded-2xl p-5 flex flex-col justify-between">
|
| 655 |
+
<div>
|
| 656 |
+
<div className="flex items-center gap-2 text-blue-600 mb-2">
|
| 657 |
+
<Info className="w-5 h-5" />
|
| 658 |
+
<h3 className="font-bold text-on-surface text-[15px] m-0">Support Engineer Operating Scope</h3>
|
| 659 |
+
</div>
|
| 660 |
+
<p className="text-on-surface-variant text-[13px] leading-relaxed m-0">
|
| 661 |
+
As a Support Engineer, you have full diagnostic visibility across platform performance metrics, customer environment states, active scans, and transaction logs.
|
| 662 |
+
</p>
|
| 663 |
+
<ul className="text-on-surface-variant text-[12.5px] mt-3 space-y-1.5 pl-4">
|
| 664 |
+
<li>• Use the <strong><Key className="w-3.5 h-3.5 inline text-primary mr-1" /> Inspect Tenant</strong> tool in Organizations tab to troubleshoot tenant issues in real-time.</li>
|
| 665 |
+
<li>• Modify client role assignments or org mappings to resolve onboarding configurations.</li>
|
| 666 |
+
<li>• Member passwords and organization creation/deletion are restricted for administrative security.</li>
|
| 667 |
+
</ul>
|
| 668 |
+
</div>
|
| 669 |
+
|
| 670 |
+
<div className="mt-4 pt-3 border-t border-blue-500/20 flex items-center justify-between text-xs text-on-surface-variant font-medium">
|
| 671 |
+
<span>System Status: Fully Operational</span>
|
| 672 |
+
<span className="text-emerald-600 font-bold flex items-center gap-1">
|
| 673 |
+
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> Online
|
| 674 |
+
</span>
|
| 675 |
+
</div>
|
| 676 |
+
</div>
|
| 677 |
+
</div>
|
| 678 |
+
|
| 679 |
+
{/* Global Transaction & Billing History */}
|
| 680 |
+
<div className="flex flex-col gap-md">
|
| 681 |
+
<h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[17px] m-0">
|
| 682 |
+
<Receipt className="w-5 h-5 text-primary mr-2" /> Global Transactions & Invoices
|
| 683 |
+
</h2>
|
| 684 |
+
<div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
|
| 685 |
+
{loading ? (
|
| 686 |
+
<div className="p-10 text-center text-on-surface-variant text-[14px]">Fetching transaction logs...</div>
|
| 687 |
+
) : (
|
| 688 |
+
<div className="overflow-x-auto max-h-[350px] hide-scrollbar">
|
| 689 |
+
<table className="w-full text-left text-sm border-collapse">
|
| 690 |
+
<thead className="bg-surface-container text-on-surface-variant border-b border-outline-variant sticky top-0 select-none">
|
| 691 |
+
<tr>
|
| 692 |
+
{['Date', 'Organization / User', 'Tier', 'Amount', 'Status', 'Invoice'].map((h, i) => (
|
| 693 |
+
<th
|
| 694 |
+
key={h}
|
| 695 |
+
onClick={() => handleBillSort(h)}
|
| 696 |
+
className={`px-md py-sm font-bold text-[12px] uppercase tracking-wider ${i === 5 ? 'text-right' : ''} ${h !== 'Invoice' ? 'cursor-pointer hover:bg-surface-container-highest transition-colors group' : ''}`}
|
| 697 |
+
>
|
| 698 |
+
<div className={`flex items-center gap-xs ${i === 5 ? 'justify-end' : ''}`}>
|
| 699 |
+
{h}
|
| 700 |
+
</div>
|
| 701 |
+
</th>
|
| 702 |
+
))}
|
| 703 |
+
</tr>
|
| 704 |
+
</thead>
|
| 705 |
+
<tbody className="divide-y divide-outline-variant">
|
| 706 |
+
{recentPayments.length === 0 ? (
|
| 707 |
+
<tr><td colSpan="6" className="p-10 text-center text-on-surface-variant">No recent transactions.</td></tr>
|
| 708 |
+
) : getSortedBills().map(p => (
|
| 709 |
+
<tr key={p.id} className="hover:bg-surface-container transition-colors">
|
| 710 |
+
<td className="px-md py-sm text-[13px] text-on-surface-variant">{new Date(p.created_at).toLocaleDateString()}</td>
|
| 711 |
+
<td className="px-md py-sm"><div className="font-bold text-on-surface text-[13px]">{p.org_name}</div></td>
|
| 712 |
+
<td className="px-md py-sm"><span className="text-[12px] font-bold capitalize text-primary bg-primary/10 px-2 py-0.5 rounded border border-primary/20">{p.tier_id}</span></td>
|
| 713 |
+
<td className="px-md py-sm font-bold text-[13.5px] text-on-surface">{p.currency === 'INR' ? '₹' : '$'}{p.amount.toLocaleString(undefined, { minimumFractionDigits: 2 })}</td>
|
| 714 |
+
<td className="px-md py-sm"><div className="flex items-center gap-xs text-[12.5px] font-bold"><span className={`w-2 h-2 rounded-full ${p.status === 'successful' ? 'bg-green-500' : 'bg-error'}`}></span><span className={p.status === 'successful' ? 'text-green-600 dark:text-green-500' : 'text-error'}>{p.status === 'successful' ? 'Success' : 'Failed'}</span></div></td>
|
| 715 |
+
<td className="px-md py-sm text-right">
|
| 716 |
+
<div className="flex items-center justify-end gap-1">
|
| 717 |
+
<button onClick={() => setViewInvoice(p)} className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1" title="View Invoice">
|
| 718 |
+
<Eye className="w-4 h-4" />
|
| 719 |
+
</button>
|
| 720 |
+
<button onClick={() => handleDownloadInvoice(p)} className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1" title="Download Invoice">
|
| 721 |
+
<Download className="w-4 h-4" />
|
| 722 |
+
</button>
|
| 723 |
+
</div>
|
| 724 |
+
</td>
|
| 725 |
+
</tr>
|
| 726 |
+
))}
|
| 727 |
+
</tbody>
|
| 728 |
+
</table>
|
| 729 |
+
</div>
|
| 730 |
+
)}
|
| 731 |
+
</div>
|
| 732 |
+
</div>
|
| 733 |
+
|
| 734 |
+
{/* System Audit Trail Preview */}
|
| 735 |
+
<div className="flex flex-col gap-md">
|
| 736 |
+
<div className="flex justify-between items-center">
|
| 737 |
+
<h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[17px] m-0">
|
| 738 |
+
<Layers className="w-5 h-5 text-primary mr-2" /> Recent System Audit Events
|
| 739 |
+
</h2>
|
| 740 |
+
<button onClick={() => setActiveTab('audit')} className="text-primary font-bold text-[13px] hover:underline bg-transparent border-0 cursor-pointer">
|
| 741 |
+
View All Logs →
|
| 742 |
+
</button>
|
| 743 |
+
</div>
|
| 744 |
+
|
| 745 |
+
<div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
|
| 746 |
+
{loading ? (
|
| 747 |
+
<div className="p-10 text-center text-on-surface-variant text-[14px]">Fetching audit logs...</div>
|
| 748 |
+
) : (
|
| 749 |
+
<div className="overflow-x-auto max-h-[350px] hide-scrollbar">
|
| 750 |
+
<table className="w-full text-left text-sm border-collapse">
|
| 751 |
+
<thead className="bg-surface-container text-on-surface-variant border-b border-outline-variant sticky top-0">
|
| 752 |
+
<tr>
|
| 753 |
+
<th className="px-md py-sm font-bold text-[12px] uppercase tracking-wider">Date & Time</th>
|
| 754 |
+
<th className="px-md py-sm font-bold text-[12px] uppercase tracking-wider">User</th>
|
| 755 |
+
<th className="px-md py-sm font-bold text-[12px] uppercase tracking-wider">Action / Event</th>
|
| 756 |
+
<th className="px-md py-sm font-bold text-[12px] uppercase tracking-wider">Target</th>
|
| 757 |
+
</tr>
|
| 758 |
+
</thead>
|
| 759 |
+
<tbody className="divide-y divide-outline-variant">
|
| 760 |
+
{auditLogs.length === 0 ? (
|
| 761 |
+
<tr><td colSpan="4" className="p-10 text-center text-on-surface-variant">No audit logs found.</td></tr>
|
| 762 |
+
) : auditLogs.slice(0, 5).map(log => (
|
| 763 |
+
<tr key={log.id} className="hover:bg-surface-container transition-colors">
|
| 764 |
+
<td className="px-md py-sm text-on-surface-variant text-[12.5px]">{new Date(log.timestamp).toLocaleString()}</td>
|
| 765 |
+
<td className="px-md py-sm font-bold text-on-surface text-[13px]">{log.user_email}</td>
|
| 766 |
+
<td className="px-md py-sm font-semibold text-on-surface text-[13px]">{log.action}</td>
|
| 767 |
+
<td className="px-md py-sm font-bold text-[13px] text-on-surface-variant">{log.target_name || log.target_id || '-'}</td>
|
| 768 |
+
</tr>
|
| 769 |
+
))}
|
| 770 |
+
</tbody>
|
| 771 |
+
</table>
|
| 772 |
+
</div>
|
| 773 |
+
)}
|
| 774 |
</div>
|
| 775 |
</div>
|
| 776 |
</div>
|
|
|
|
| 1134 |
</div>
|
| 1135 |
)}
|
| 1136 |
|
| 1137 |
+
{/* Active Scans Modal */}
|
| 1138 |
+
<CustomModal isOpen={activeScansModalOpen} onClose={() => setActiveScansModalOpen(false)} title="Active Scans Diagnostic">
|
| 1139 |
+
{fetchingActiveScans ? <div className="text-center py-xl font-bold text-on-surface-variant">Loading active scans...</div> : activeScans.length === 0 ? <div className="text-center py-xl font-bold text-on-surface-variant">No active scans running right now.</div> : (
|
| 1140 |
+
<div className="flex flex-col gap-md">
|
| 1141 |
+
{activeScans.map(scan => (
|
| 1142 |
+
<div key={scan.id} className="bg-surface border border-outline-variant rounded-xl p-md flex flex-col sm:flex-row justify-between items-center gap-md">
|
| 1143 |
+
<div>
|
| 1144 |
+
<div className="font-bold text-on-surface">{scan.target_url}</div>
|
| 1145 |
+
<div className="text-xs text-on-surface-variant mt-0.5">Scan ID: {scan.id} | Org ID: {scan.org_id}</div>
|
| 1146 |
+
</div>
|
| 1147 |
+
<button onClick={() => handleKillScan(scan.id)} className="bg-error/10 text-error border border-error/20 py-1.5 px-3 rounded font-bold cursor-pointer hover:bg-error/20 transition-all text-xs">
|
| 1148 |
+
Terminate Scan
|
| 1149 |
+
</button>
|
| 1150 |
+
</div>
|
| 1151 |
+
))}
|
| 1152 |
+
</div>
|
| 1153 |
+
)}
|
| 1154 |
+
</CustomModal>
|
| 1155 |
+
|
| 1156 |
+
{/* Invoice View Modal */}
|
| 1157 |
+
<CustomModal
|
| 1158 |
+
isOpen={!!viewInvoice}
|
| 1159 |
+
onClose={() => setViewInvoice(null)}
|
| 1160 |
+
title="Invoice Details"
|
| 1161 |
+
footer={
|
| 1162 |
+
<>
|
| 1163 |
+
<button onClick={() => setViewInvoice(null)} className="px-4 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg font-bold border-0 bg-transparent cursor-pointer">Close</button>
|
| 1164 |
+
<button onClick={() => { handleDownloadInvoice(viewInvoice); setViewInvoice(null); }} className="px-4 py-2 bg-primary text-white flex items-center gap-2 rounded-lg font-bold border-0 cursor-pointer">
|
| 1165 |
+
<Download className="w-4 h-4" /> Download PDF
|
| 1166 |
+
</button>
|
| 1167 |
+
</>
|
| 1168 |
+
}
|
| 1169 |
+
>
|
| 1170 |
+
{viewInvoice && (
|
| 1171 |
+
<div className="flex flex-col gap-4 text-on-surface">
|
| 1172 |
+
<div className="flex justify-between items-end border-b border-outline-variant pb-4 mb-2">
|
| 1173 |
+
<div>
|
| 1174 |
+
<h1 className="text-2xl font-bold text-primary m-0">LarShield</h1>
|
| 1175 |
+
<p className="text-on-surface-variant m-0 text-sm mt-1">Payment Receipt & Invoice</p>
|
| 1176 |
+
</div>
|
| 1177 |
+
<div className="text-right">
|
| 1178 |
+
<div className="text-sm font-bold opacity-80 uppercase tracking-wider">Date</div>
|
| 1179 |
+
<div>{new Date(viewInvoice.created_at).toLocaleString()}</div>
|
| 1180 |
+
</div>
|
| 1181 |
+
</div>
|
| 1182 |
+
|
| 1183 |
+
<div className="grid grid-cols-2 gap-4 bg-surface-container-low p-4 rounded-lg">
|
| 1184 |
+
<div>
|
| 1185 |
+
<div className="text-xs text-on-surface-variant font-bold uppercase mb-1">Billed To</div>
|
| 1186 |
+
<div className="font-bold">{viewInvoice.org_name}</div>
|
| 1187 |
+
<div className="text-sm opacity-80">{viewInvoice.user_email}</div>
|
| 1188 |
+
</div>
|
| 1189 |
+
<div className="text-right">
|
| 1190 |
+
<div className="text-xs text-on-surface-variant font-bold uppercase mb-1">Status</div>
|
| 1191 |
+
<div className={`font-bold uppercase ${viewInvoice.status === 'successful' ? 'text-green-600' : 'text-error'}`}>{viewInvoice.status}</div>
|
| 1192 |
+
</div>
|
| 1193 |
+
</div>
|
| 1194 |
+
|
| 1195 |
+
<div className="border border-outline-variant rounded-lg overflow-hidden">
|
| 1196 |
+
<table className="w-full text-left border-collapse text-sm">
|
| 1197 |
+
<thead className="bg-surface-container border-b border-outline-variant">
|
| 1198 |
+
<tr>
|
| 1199 |
+
<th className="p-3 font-bold">Subscription Plan</th>
|
| 1200 |
+
<th className="p-3 font-bold text-right">Amount</th>
|
| 1201 |
+
</tr>
|
| 1202 |
+
</thead>
|
| 1203 |
+
<tbody>
|
| 1204 |
+
<tr>
|
| 1205 |
+
<td className="p-3 font-semibold capitalize">{viewInvoice.tier_id} Plan Subscription</td>
|
| 1206 |
+
<td className="p-3 font-bold text-right">{viewInvoice.currency === 'INR' ? '₹' : '$'}{viewInvoice.amount.toLocaleString(undefined, { minimumFractionDigits: 2 })}</td>
|
| 1207 |
+
</tr>
|
| 1208 |
+
</tbody>
|
| 1209 |
+
</table>
|
| 1210 |
+
</div>
|
| 1211 |
+
|
| 1212 |
+
<div className="flex justify-between items-center bg-primary/10 border border-primary/20 p-4 rounded-lg text-primary font-bold text-lg mt-2">
|
| 1213 |
+
<span>Total Amount Paid</span>
|
| 1214 |
+
<span>{viewInvoice.currency === 'INR' ? '₹' : '$'}{viewInvoice.amount.toLocaleString(undefined, { minimumFractionDigits: 2 })}</span>
|
| 1215 |
+
</div>
|
| 1216 |
+
</div>
|
| 1217 |
+
)}
|
| 1218 |
+
</CustomModal>
|
| 1219 |
+
|
| 1220 |
{/* Prompt Modal for Editing Role/Org */}
|
| 1221 |
<CustomModal
|
| 1222 |
isOpen={promptModal.isOpen}
|
|
|
|
| 1270 |
footer={
|
| 1271 |
<>
|
| 1272 |
<button onClick={closeConfirm} className="px-4 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg font-bold border-0 bg-transparent cursor-pointer">Cancel</button>
|
| 1273 |
+
<button onClick={confirmModal.onConfirm} className={`px-4 py-2 rounded-lg font-bold border-0 cursor-pointer text-white ${confirmModal.type === 'error' ? 'bg-error' : 'bg-primary'}`}>Confirm</button>
|
| 1274 |
</>
|
| 1275 |
}
|
| 1276 |
/>
|