larxius commited on
Commit
688e619
·
verified ·
1 Parent(s): 53c3fd2

Update frontend/src/pages/SupportEngineerPanel.jsx

Browse files
frontend/src/pages/SupportEngineerPanel.jsx ADDED
@@ -0,0 +1,930 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useAuth } from '../components/AuthContext';
3
+ import { Link, useNavigate } from 'react-router-dom';
4
+ import toast from 'react-hot-toast';
5
+ import {
6
+ Users,
7
+ Shield,
8
+ Search,
9
+ Filter,
10
+ X,
11
+ Edit,
12
+ Key,
13
+ Activity,
14
+ Layers,
15
+ Mail,
16
+ Calendar,
17
+ RefreshCw,
18
+ CheckCircle2,
19
+ AlertTriangle,
20
+ Info
21
+ } from 'lucide-react';
22
+ import { CustomModal } from '../components/CustomModal';
23
+
24
+ const TablePagination = ({ currentPage, totalEntries, pageSize, onPageChange, onPageSizeChange }) => {
25
+ const totalPages = Math.ceil(totalEntries / pageSize) || 1;
26
+ const validCurrentPage = Math.min(Math.max(1, currentPage), totalPages);
27
+ const startIndex = (validCurrentPage - 1) * pageSize;
28
+ const endIndex = Math.min(startIndex + pageSize, totalEntries);
29
+
30
+ return (
31
+ <div className="p-4 border-t border-outline-variant/60 bg-surface-container-lowest flex flex-col sm:flex-row items-center justify-between gap-3 text-[13px] text-on-surface-variant">
32
+ <div className="flex items-center gap-2">
33
+ <span className="font-medium">Rows per page:</span>
34
+ <select
35
+ value={pageSize}
36
+ onChange={(e) => {
37
+ onPageSizeChange(Number(e.target.value));
38
+ onPageChange(1);
39
+ }}
40
+ className="bg-surface border border-outline-variant/60 text-on-surface rounded px-2 py-1 text-[12px] font-bold focus:outline-none cursor-pointer"
41
+ >
42
+ <option value={10}>10</option>
43
+ <option value={25}>25</option>
44
+ <option value={50}>50</option>
45
+ <option value={100}>100</option>
46
+ </select>
47
+ <span className="ml-2 font-medium">
48
+ {totalEntries === 0 ? '0 of 0 records' : `${startIndex + 1} - ${endIndex} of ${totalEntries} records`}
49
+ </span>
50
+ </div>
51
+
52
+ <div className="flex items-center gap-1.5">
53
+ <button
54
+ onClick={() => onPageChange(Math.max(1, validCurrentPage - 1))}
55
+ disabled={validCurrentPage === 1}
56
+ className="px-3 py-1.5 rounded-lg border border-outline-variant/60 bg-surface hover:bg-surface-container-high text-on-surface font-bold text-[12px] disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer transition-colors"
57
+ >
58
+ Previous
59
+ </button>
60
+
61
+ <div className="flex items-center gap-1 px-1">
62
+ {Array.from({ length: totalPages }, (_, i) => i + 1)
63
+ .filter(p => p === 1 || p === totalPages || Math.abs(p - validCurrentPage) <= 1)
64
+ .map((page, idx, arr) => {
65
+ const prev = arr[idx - 1];
66
+ return (
67
+ <React.Fragment key={page}>
68
+ {prev && page - prev > 1 && <span className="px-1 text-on-surface-variant text-[12px]">...</span>}
69
+ <button
70
+ onClick={() => onPageChange(page)}
71
+ className={`w-7 h-7 rounded-lg text-[12px] font-bold transition-colors cursor-pointer ${
72
+ validCurrentPage === page
73
+ ? 'bg-primary text-on-primary shadow-2xs'
74
+ : 'bg-surface hover:bg-surface-container-high border border-outline-variant/60 text-on-surface'
75
+ }`}
76
+ >
77
+ {page}
78
+ </button>
79
+ </React.Fragment>
80
+ );
81
+ })}
82
+ </div>
83
+
84
+ <button
85
+ onClick={() => onPageChange(Math.min(totalPages, validCurrentPage + 1))}
86
+ disabled={validCurrentPage === totalPages}
87
+ className="px-3 py-1.5 rounded-lg border border-outline-variant/60 bg-surface hover:bg-surface-container-high text-on-surface font-bold text-[12px] disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer transition-colors"
88
+ >
89
+ Next
90
+ </button>
91
+ </div>
92
+ </div>
93
+ );
94
+ };
95
+
96
+ const MEMBER_ROLE_OPTIONS = [
97
+ { label: 'Admin', value: 'admin' },
98
+ { label: 'SOC Analyst', value: 'soc_analyst' },
99
+ { label: 'Organization Admin', value: 'org_admin' },
100
+ { label: 'Executive User', value: 'executive_user' },
101
+ { label: 'Super Admin', value: 'super_admin' },
102
+ { label: 'Support Engineer', value: 'support_engineer' },
103
+ { label: 'Read Only', value: 'read_only' }
104
+ ];
105
+
106
+ export const SupportEngineerPanel = () => {
107
+ const { user, refreshAccessToken } = useAuth();
108
+ const navigate = useNavigate();
109
+
110
+ const authFetch = async (url, options = {}) => {
111
+ let activeToken = localStorage.getItem('wss_token');
112
+ const headers = {
113
+ 'Authorization': `Bearer ${activeToken}`,
114
+ ...(options.headers || {})
115
+ };
116
+
117
+ let res = await fetch(url, { ...options, headers });
118
+
119
+ if (res.status === 401 && refreshAccessToken) {
120
+ const newToken = await refreshAccessToken();
121
+ if (newToken) {
122
+ headers['Authorization'] = `Bearer ${newToken}`;
123
+ res = await fetch(url, { ...options, headers });
124
+ }
125
+ }
126
+ return res;
127
+ };
128
+
129
+ const [organizations, setOrganizations] = useState([]);
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);
136
+
137
+ const [activeTab, setActiveTab] = useState(() => {
138
+ try {
139
+ const params = new URLSearchParams(window.location.search);
140
+ return params.get('tab') || 'organizations';
141
+ } catch (e) {
142
+ return 'organizations';
143
+ }
144
+ });
145
+
146
+ // Table sorting states
147
+ const [sortOrgCol, setSortOrgCol] = useState('Tenant Name');
148
+ const [sortOrgDir, setSortOrgDir] = useState('asc');
149
+
150
+ const [sortUserCol, setSortUserCol] = useState('Email');
151
+ const [sortUserDir, setSortUserDir] = useState('asc');
152
+
153
+ // Filter states for Global Members
154
+ const [userSearch, setUserSearch] = useState('');
155
+ const [userRoleFilter, setUserRoleFilter] = useState('all');
156
+ const [userOrgFilter, setUserOrgFilter] = useState('all');
157
+
158
+ // Pagination states
159
+ const [orgPage, setOrgPage] = useState(1);
160
+ const [orgPageSize, setOrgPageSize] = useState(25);
161
+
162
+ const [userPage, setUserPage] = useState(1);
163
+ const [userPageSize, setUserPageSize] = useState(25);
164
+
165
+ const [auditPage, setAuditPage] = useState(1);
166
+ const [auditPageSize, setAuditPageSize] = useState(25);
167
+
168
+ const [bookingPage, setBookingPage] = useState(1);
169
+ const [bookingPageSize, setBookingPageSize] = useState(25);
170
+
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
+
184
+ const fetchStats = async () => {
185
+ setLoading(true);
186
+ try {
187
+ const activeToken = localStorage.getItem('wss_token');
188
+ if (!activeToken) {
189
+ setLoading(false);
190
+ return;
191
+ }
192
+ const [globalRes, bookingsRes, emailLogsRes] = await Promise.all([
193
+ authFetch('/api/auth/global-stats'),
194
+ authFetch('/api/demo/bookings'),
195
+ authFetch('/api/auth/email-logs')
196
+ ]);
197
+
198
+ if (globalRes && globalRes.ok) {
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
+ }
205
+ if (bookingsRes && bookingsRes.ok) {
206
+ const data = await bookingsRes.json();
207
+ setDemoBookings(Array.isArray(data.bookings) ? data.bookings : []);
208
+ }
209
+ if (emailLogsRes && emailLogsRes.ok) {
210
+ const data = await emailLogsRes.json();
211
+ setEmailLogs(Array.isArray(data.logs) ? data.logs : []);
212
+ }
213
+ } catch (err) {
214
+ console.error("SupportEngineerPanel fetch error:", err);
215
+ } finally {
216
+ setLoading(false);
217
+ }
218
+ };
219
+
220
+ useEffect(() => {
221
+ fetchStats();
222
+ }, []);
223
+
224
+ const handleOrgSort = (column) => {
225
+ if (column === 'Quotas' || column === 'Actions') return;
226
+ if (sortOrgCol === column) {
227
+ setSortOrgDir(sortOrgDir === 'asc' ? 'desc' : 'asc');
228
+ } else {
229
+ setSortOrgCol(column);
230
+ setSortOrgDir('asc');
231
+ }
232
+ };
233
+
234
+ const getSortedOrgs = () => {
235
+ return [...(organizations || [])].sort((a, b) => {
236
+ let aVal, bVal;
237
+ switch (sortOrgCol) {
238
+ case 'Tenant Name': aVal = a.name || ''; bVal = b.name || ''; break;
239
+ case 'Tier': aVal = a.tier || a.subscription_tier || ''; bVal = b.tier || b.subscription_tier || ''; break;
240
+ case 'Status': aVal = a.status || (a.is_active ? 'active' : 'inactive'); bVal = b.status || (b.is_active ? 'active' : 'inactive'); break;
241
+ default: aVal = a.name || ''; bVal = b.name || '';
242
+ }
243
+ if (aVal < bVal) return sortOrgDir === 'asc' ? -1 : 1;
244
+ if (aVal > bVal) return sortOrgDir === 'asc' ? 1 : -1;
245
+ return 0;
246
+ });
247
+ };
248
+
249
+ const handleUserSort = (column) => {
250
+ if (column === 'Actions') return;
251
+ if (sortUserCol === column) {
252
+ setSortUserDir(sortUserDir === 'asc' ? 'desc' : 'asc');
253
+ } else {
254
+ setSortUserCol(column);
255
+ setSortUserDir('asc');
256
+ }
257
+ };
258
+
259
+ const getFilteredUsers = () => {
260
+ return (users || []).filter(u => {
261
+ const emailMatch = !userSearch || u.email?.toLowerCase().includes(userSearch.toLowerCase());
262
+ const roleMatch = userRoleFilter === 'all' || u.role === userRoleFilter;
263
+ const orgMatch = userOrgFilter === 'all' || (
264
+ userOrgFilter === 'no_org' ? !u.org_id : String(u.org_id) === String(userOrgFilter)
265
+ );
266
+ return emailMatch && roleMatch && orgMatch;
267
+ });
268
+ };
269
+
270
+ const getSortedUsers = () => {
271
+ const filtered = getFilteredUsers();
272
+ return [...filtered].sort((a, b) => {
273
+ let aVal, bVal;
274
+ switch (sortUserCol) {
275
+ case 'Email': aVal = a.email || ''; bVal = b.email || ''; break;
276
+ case 'Role': aVal = a.role || ''; bVal = b.role || ''; break;
277
+ case 'Organization': aVal = a.org_name || ''; bVal = b.org_name || ''; break;
278
+ default: aVal = a.email || ''; bVal = b.email || '';
279
+ }
280
+ if (aVal < bVal) return sortUserDir === 'asc' ? -1 : 1;
281
+ if (aVal > bVal) return sortUserDir === 'asc' ? 1 : -1;
282
+ return 0;
283
+ });
284
+ };
285
+
286
+ const clearUserFilters = () => {
287
+ setUserSearch('');
288
+ setUserRoleFilter('all');
289
+ setUserOrgFilter('all');
290
+ setUserPage(1);
291
+ };
292
+
293
+ const handleImpersonate = (orgId, orgName) => {
294
+ setConfirmModal({
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}`, {
301
+ method: 'POST'
302
+ });
303
+ if (res.ok) {
304
+ const data = await res.json();
305
+ localStorage.setItem('original_admin_token', localStorage.getItem('wss_token'));
306
+ localStorage.setItem('wss_token', data.access_token);
307
+ window.location.href = '/dashboard';
308
+ } else {
309
+ toast.error('Failed to inspect tenant environment');
310
+ }
311
+ } catch (err) {
312
+ toast.error('Network error during environment inspection');
313
+ }
314
+ closeConfirm();
315
+ }
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: '' },
323
+ ...(organizations || []).map(o => ({ label: o.name, value: String(o.id) }))
324
+ ];
325
+
326
+ let initialOrgId = (u.org_id !== undefined && u.org_id !== null) ? String(u.org_id) : '';
327
+ if (!initialOrgId && u.org_name && u.org_name !== 'No Org (Super Admin)' && organizations) {
328
+ const match = organizations.find(o => o.name === u.org_name);
329
+ if (match) initialOrgId = String(match.id);
330
+ }
331
+
332
+ setPromptValues({
333
+ email: u.email,
334
+ role: u.role || 'admin',
335
+ org_id: initialOrgId
336
+ });
337
+
338
+ setPromptModal({
339
+ isOpen: true,
340
+ title: 'Edit Member Role & Organization',
341
+ desc: `Update role or organization assignment for ${u.email}`,
342
+ inputs: [
343
+ { key: 'email', label: 'User Email', disabled: true },
344
+ { key: 'role', label: 'Role', type: 'select', options: MEMBER_ROLE_OPTIONS },
345
+ { key: 'org_id', label: 'Organization', type: 'select', options: orgOptions }
346
+ ],
347
+ onConfirm: async (vals) => {
348
+ try {
349
+ const res = await authFetch(`/api/auth/users/${u.id}`, {
350
+ method: 'PUT',
351
+ headers: { 'Content-Type': 'application/json' },
352
+ body: JSON.stringify({
353
+ role: vals.role,
354
+ org_id: vals.org_id || null
355
+ })
356
+ });
357
+ const data = await res.json().catch(() => ({}));
358
+ if (res.ok) {
359
+ toast.success('Member role & organization updated successfully');
360
+ fetchStats();
361
+ } else {
362
+ toast.error(data.message || 'Failed to update member');
363
+ }
364
+ } catch (err) {
365
+ toast.error('Network error updating member');
366
+ }
367
+ closePrompt();
368
+ }
369
+ });
370
+ };
371
+
372
+ return (
373
+ <div className="flex flex-col gap-lg w-full text-left font-body">
374
+
375
+ {/* Header Banner */}
376
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-md border-b border-outline-variant pb-md">
377
+ <div>
378
+ <div className="flex items-center gap-2">
379
+ <h1 className="font-headline-md font-bold text-on-surface tracking-tight text-[22px] m-0">
380
+ Support Engineer Operations
381
+ </h1>
382
+ <span className="px-2.5 py-0.5 rounded-full text-[11px] font-extrabold uppercase bg-blue-500/10 text-blue-600 border border-blue-500/30">
383
+ SUPPORT CONSOLE
384
+ </span>
385
+ </div>
386
+ <p className="font-body-md text-on-surface-variant text-[13.5px] mt-1 m-0">
387
+ Client environment inspection, troubleshooting assistance, audit logs, and member role mappings.
388
+ </p>
389
+ </div>
390
+
391
+ <div className="flex flex-wrap items-center gap-2">
392
+ <button
393
+ onClick={fetchStats}
394
+ className="flex items-center gap-1.5 px-3 py-1.5 bg-surface-container border border-outline-variant hover:bg-surface-container-high text-on-surface rounded-lg font-bold text-[12.5px] cursor-pointer transition-all"
395
+ >
396
+ <RefreshCw className={`w-4 h-4 text-primary ${loading ? 'animate-spin' : ''}`} />
397
+ Sync Metrics
398
+ </button>
399
+ <button
400
+ onClick={() => navigate('/dashboard')}
401
+ className="flex items-center gap-1.5 px-3 py-1.5 bg-primary/10 border border-primary/20 text-primary hover:bg-primary/20 rounded-lg font-bold text-[12.5px] cursor-pointer transition-all"
402
+ >
403
+ <Activity className="w-4 h-4" />
404
+ Org Dashboard
405
+ </button>
406
+ <button
407
+ onClick={() => navigate('/super-admin/logs')}
408
+ className="flex items-center gap-1.5 px-3 py-1.5 bg-surface-container border border-outline-variant hover:bg-surface-container-high text-on-surface rounded-lg font-bold text-[12.5px] cursor-pointer transition-all"
409
+ >
410
+ <Shield className="w-4 h-4 text-primary" />
411
+ Logs & Threats
412
+ </button>
413
+ </div>
414
+ </div>
415
+
416
+ {/* Tabs Navigation */}
417
+ <div className="flex items-center gap-1 border-b border-outline-variant overflow-x-auto hide-scrollbar">
418
+ {[
419
+ { id: 'overview', label: 'Overview', icon: Activity },
420
+ { id: 'organizations', label: 'Organizations', icon: Shield },
421
+ { id: 'members', label: 'Members', icon: Users },
422
+ { id: 'audit', label: 'Audit Logs', icon: Layers },
423
+ { id: 'bookings', label: 'Bookings', icon: Calendar },
424
+ { id: 'emails', label: 'Emails', icon: Mail }
425
+ ].map(tab => {
426
+ const IconComp = tab.icon;
427
+ return (
428
+ <button
429
+ key={tab.id}
430
+ onClick={() => setActiveTab(tab.id)}
431
+ className={`flex items-center gap-2 px-4 py-2.5 font-bold text-[13.5px] border-b-2 transition-all cursor-pointer bg-transparent border-t-0 border-x-0 ${
432
+ activeTab === tab.id
433
+ ? 'border-primary text-primary bg-primary/5 rounded-t-lg'
434
+ : 'border-transparent text-on-surface-variant hover:text-on-surface hover:bg-surface-container/50'
435
+ }`}
436
+ >
437
+ <IconComp className="w-4 h-4" />
438
+ {tab.label}
439
+ </button>
440
+ );
441
+ })}
442
+ </div>
443
+
444
+ {/* OVERVIEW TAB */}
445
+ {activeTab === 'overview' && (
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
+ <Shield className="w-5 h-5" />
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
+ <CheckCircle2 className="w-5 h-5" />
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
+ <Activity className="w-5 h-5" />
486
+ </div>
487
+ </div>
488
+ </div>
489
+
490
+ {/* Operational Support Guidance Box */}
491
+ <div className="bg-blue-500/5 border border-blue-500/20 rounded-2xl p-5 flex items-start gap-4">
492
+ <div className="w-10 h-10 rounded-xl bg-blue-500/10 text-blue-600 flex items-center justify-center shrink-0 mt-0.5">
493
+ <Info className="w-5 h-5" />
494
+ </div>
495
+ <div className="flex-1">
496
+ <h3 className="font-bold text-on-surface text-[15px] m-0 mb-1">Support Engineer Scope & Guidelines</h3>
497
+ <p className="text-on-surface-variant text-[13px] leading-relaxed m-0">
498
+ You are currently in the Support Engineer Operations Console. You have full visibility into organizations, active scan quotas, global members, audit logs, and outbound communication logs to assist client troubleshooting.
499
+ </p>
500
+ <ul className="text-on-surface-variant text-[12.5px] mt-2 mb-0 pl-4 space-y-1">
501
+ <li>• Use the <strong><Key className="w-3.5 h-3.5 inline mr-1 text-primary" /> Inspect</strong> action on any client organization to temporarily log in as their admin and diagnose issue reports.</li>
502
+ <li>• You can edit member roles or organization assignments to assist configuration, but password changes are restricted for security.</li>
503
+ <li>• Member and Organization creation/deletion are managed exclusively by Super Admins.</li>
504
+ </ul>
505
+ </div>
506
+ </div>
507
+ </div>
508
+ )}
509
+
510
+ {/* ORGANIZATIONS TAB */}
511
+ {activeTab === 'organizations' && (
512
+ <div className="flex flex-col gap-md mb-xl animate-fade-in">
513
+ <div className="flex justify-between items-center">
514
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px]">
515
+ <Shield className="w-5 h-5 text-primary mr-2" /> Client Organizations Directory
516
+ </h2>
517
+ </div>
518
+
519
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto hide-scrollbar">
520
+ {loading ? <div className="p-10 text-center text-on-surface-variant">Fetching directory...</div> : (
521
+ <table className="w-full text-left text-sm border-collapse">
522
+ <thead className="bg-surface-container text-on-surface-variant border-b border-outline-variant select-none">
523
+ <tr>
524
+ {['Tenant Name', 'Tier', 'Status', 'Quotas', 'Actions'].map((h, i) => (
525
+ <th
526
+ key={h}
527
+ onClick={() => handleOrgSort(h)}
528
+ className={`px-md py-sm font-bold text-[12px] uppercase tracking-wider ${i === 4 ? 'text-right' : ''} ${(h !== 'Actions' && h !== 'Quotas') ? 'cursor-pointer hover:bg-surface-container-highest transition-colors group' : ''}`}
529
+ >
530
+ <div className={`flex items-center gap-xs ${i === 4 ? 'justify-end' : ''}`}>
531
+ {h}
532
+ </div>
533
+ </th>
534
+ ))}
535
+ </tr>
536
+ </thead>
537
+ <tbody className="divide-y divide-outline-variant">
538
+ {getSortedOrgs().slice((orgPage - 1) * orgPageSize, orgPage * orgPageSize).map((org) => (
539
+ <tr key={org.id} className="hover:bg-surface-container transition-colors group">
540
+ <td className="px-md py-sm"><div className="font-bold text-on-surface text-[14px]">{org.name}</div></td>
541
+ <td className="px-md py-sm"><span className="px-2 py-0.5 rounded border text-[11px] font-bold tracking-wide bg-surface-container-high border-outline-variant text-on-surface-variant">{org.tier || org.subscription_tier}</span></td>
542
+ <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 ${org.status === 'active' || org.is_active ? 'bg-green-500' : 'bg-red-500'}`}></span><span className={org.status === 'active' || org.is_active ? 'text-green-600 dark:text-green-500' : 'text-error'}>{org.status ? org.status.charAt(0).toUpperCase() + org.status.slice(1) : (org.is_active ? 'Active' : 'Inactive')}</span></div></td>
543
+ <td className="px-md py-sm">
544
+ <div className="flex flex-nowrap gap-2 items-center">
545
+ {org.quotas?.map((q, idx) => {
546
+ const remaining = q.allocated_count === -1 ? '∞' : Math.max(0, q.allocated_count - (q.used_count || 0));
547
+ const style = q.scan_type === 'Deep' ? 'bg-orange-500/10 text-orange-600 border-orange-500/30' :
548
+ q.scan_type === 'Advanced' ? 'bg-purple-500/10 text-purple-600 border-purple-500/30' :
549
+ 'bg-blue-500/10 text-blue-600 border-blue-500/30';
550
+ return (
551
+ <div key={idx} className={`text-[10.5px] font-bold px-2 py-0.5 rounded border flex items-center gap-1 shadow-sm ${style}`}>
552
+ <span className="uppercase opacity-90 tracking-wider">{q.scan_type}:</span>
553
+ <span className="text-[12px]">{remaining}</span>
554
+ </div>
555
+ );
556
+ })}
557
+ </div>
558
+ </td>
559
+ <td className="px-md py-sm text-right">
560
+ <button onClick={() => handleImpersonate(org.id, org.name)} className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1" title="Inspect Customer Environment / Assist Troubleshooting">
561
+ <span className="material-symbols-outlined text-[18px]">vpn_key</span>
562
+ </button>
563
+ </td>
564
+ </tr>
565
+ ))}
566
+ </tbody>
567
+ </table>
568
+ )}
569
+ <TablePagination
570
+ currentPage={orgPage}
571
+ totalEntries={organizations.length}
572
+ pageSize={orgPageSize}
573
+ onPageChange={setOrgPage}
574
+ onPageSizeChange={setOrgPageSize}
575
+ />
576
+ </div>
577
+ </div>
578
+ )}
579
+
580
+ {/* MEMBERS TAB */}
581
+ {activeTab === 'members' && (
582
+ <div className="flex flex-col gap-md mb-xl animate-fade-in">
583
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-md mb-xs">
584
+ <div>
585
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px]">
586
+ <Users className="w-5 h-5 text-primary mr-2" /> Global Members
587
+ </h2>
588
+ <p className="text-[13px] text-on-surface-variant mt-0.5">
589
+ Inspect user accounts, assign roles, and modify client organization mapping.
590
+ </p>
591
+ </div>
592
+ </div>
593
+
594
+ {/* Filter & Search Bar */}
595
+ <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">
596
+ <div className="flex flex-wrap items-center gap-3 w-full flex-1">
597
+ <div className="relative w-full sm:w-72">
598
+ <Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant" />
599
+ <input
600
+ type="text"
601
+ placeholder="Search email, org, role..."
602
+ value={userSearch}
603
+ onChange={(e) => {
604
+ setUserSearch(e.target.value);
605
+ setUserPage(1);
606
+ }}
607
+ className="w-full bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg pl-9 pr-8 py-2 focus:outline-none focus:border-primary placeholder:text-on-surface-variant/50"
608
+ />
609
+ {userSearch && (
610
+ <button onClick={() => setUserSearch('')} 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">
611
+ <X className="w-3.5 h-3.5" />
612
+ </button>
613
+ )}
614
+ </div>
615
+
616
+ <div className="flex items-center gap-2 w-full sm:w-auto">
617
+ <Filter className="w-4 h-4 text-on-surface-variant shrink-0" />
618
+ <select
619
+ value={userRoleFilter}
620
+ onChange={(e) => {
621
+ setUserRoleFilter(e.target.value);
622
+ setUserPage(1);
623
+ }}
624
+ 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"
625
+ >
626
+ <option value="all">All Roles</option>
627
+ {MEMBER_ROLE_OPTIONS.map(r => (
628
+ <option key={r.value} value={r.value}>{r.label}</option>
629
+ ))}
630
+ </select>
631
+
632
+ <select
633
+ value={userOrgFilter}
634
+ onChange={(e) => {
635
+ setUserOrgFilter(e.target.value);
636
+ setUserPage(1);
637
+ }}
638
+ 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"
639
+ >
640
+ <option value="all">All Organizations</option>
641
+ <option value="no_org">No Org (Super Admin / Global)</option>
642
+ {(organizations || []).map(org => (
643
+ <option key={org.id} value={org.id}>{org.name}</option>
644
+ ))}
645
+ </select>
646
+ </div>
647
+
648
+ {(userSearch || userRoleFilter !== 'all' || userOrgFilter !== 'all') && (
649
+ <button
650
+ onClick={clearUserFilters}
651
+ 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"
652
+ >
653
+ <X className="w-4 h-4" />
654
+ Reset Filters
655
+ </button>
656
+ )}
657
+ </div>
658
+ </div>
659
+
660
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto hide-scrollbar">
661
+ {loading ? <div className="p-10 text-center text-on-surface-variant">Fetching users...</div> : getSortedUsers().length === 0 ? (
662
+ <div className="p-12 text-center text-on-surface-variant">
663
+ <Users className="w-8 h-8 text-on-surface-variant/40 mx-auto mb-2" />
664
+ <p className="font-bold text-[14px] text-on-surface">No members match your filter criteria.</p>
665
+ <p className="text-[12.5px] text-on-surface-variant mt-1">Try clearing your search terms or filter criteria.</p>
666
+ {(userSearch || userRoleFilter !== 'all' || userOrgFilter !== 'all') && (
667
+ <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">
668
+ Clear All Filters
669
+ </button>
670
+ )}
671
+ </div>
672
+ ) : (
673
+ <table className="w-full text-left text-sm border-collapse">
674
+ <thead className="bg-surface-container text-on-surface-variant border-b border-outline-variant select-none">
675
+ <tr>
676
+ {['Email', 'Role', 'Organization', 'Actions'].map((h, i) => (
677
+ <th
678
+ key={h}
679
+ onClick={() => handleUserSort(h)}
680
+ className={`px-md py-sm font-bold text-[12px] uppercase tracking-wider ${i === 3 ? 'text-right' : ''} ${h !== 'Actions' ? 'cursor-pointer hover:bg-surface-container-highest transition-colors group' : ''}`}
681
+ >
682
+ <div className={`flex items-center gap-xs ${i === 3 ? 'justify-end' : ''}`}>
683
+ {h}
684
+ </div>
685
+ </th>
686
+ ))}
687
+ </tr>
688
+ </thead>
689
+ <tbody className="divide-y divide-outline-variant">
690
+ {getSortedUsers().slice((userPage - 1) * userPageSize, userPage * userPageSize).map((u) => (
691
+ <tr key={u.id} className="hover:bg-surface-container transition-colors group">
692
+ <td className="px-md py-sm font-bold text-on-surface text-[14px]">{u.email}</td>
693
+ <td className="px-md py-sm">
694
+ <span className={`px-2.5 py-1 rounded-md border text-[11px] font-extrabold tracking-wide uppercase ${
695
+ u.role === 'super_admin' ? 'bg-purple-500/10 text-purple-500 border-purple-500/30' :
696
+ u.role === 'support_engineer' ? 'bg-blue-500/10 text-blue-500 border-blue-500/30' :
697
+ u.role === 'soc_analyst' ? 'bg-emerald-500/10 text-emerald-600 border-emerald-500/30' :
698
+ u.role === 'org_admin' || u.role === 'admin' ? 'bg-primary/10 text-primary border-primary/30' :
699
+ 'bg-surface-container-high border-outline-variant text-on-surface-variant'
700
+ }`}>
701
+ {u.role ? u.role.replace(/_/g, ' ') : 'User'}
702
+ </span>
703
+ </td>
704
+ <td className="px-md py-sm text-[13px] font-semibold text-on-surface-variant">{u.org_name}</td>
705
+ <td className="px-md py-sm text-right flex justify-end gap-2">
706
+ <button
707
+ onClick={() => handleEditMember(u)}
708
+ className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1"
709
+ title="Edit Member Role & Organization"
710
+ >
711
+ <Edit className="w-4 h-4" />
712
+ </button>
713
+ </td>
714
+ </tr>
715
+ ))}
716
+ </tbody>
717
+ </table>
718
+ )}
719
+ <TablePagination
720
+ currentPage={userPage}
721
+ totalEntries={getSortedUsers().length}
722
+ pageSize={userPageSize}
723
+ onPageChange={setUserPage}
724
+ onPageSizeChange={setUserPageSize}
725
+ />
726
+ </div>
727
+ </div>
728
+ )}
729
+
730
+ {/* AUDIT LOGS TAB */}
731
+ {activeTab === 'audit' && (
732
+ <div className="flex flex-col gap-md mb-xl animate-fade-in">
733
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] mb-md">
734
+ <Layers className="w-5 h-5 text-primary mr-2" /> Global Audit Logs
735
+ </h2>
736
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto">
737
+ {auditLogs.length === 0 ? (
738
+ <div className="p-xl text-center text-on-surface-variant font-bold">No audit logs available.</div>
739
+ ) : (
740
+ <>
741
+ <table className="w-full text-left border-collapse">
742
+ <thead className="bg-surface border-b border-outline-variant text-on-surface-variant">
743
+ <tr>
744
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Timestamp</th>
745
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Action</th>
746
+ <th className="px-md py-sm font-bold text-[12px] uppercase">User Email</th>
747
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Target</th>
748
+ </tr>
749
+ </thead>
750
+ <tbody className="divide-y divide-outline-variant">
751
+ {auditLogs.slice((auditPage - 1) * auditPageSize, auditPage * auditPageSize).map((log) => (
752
+ <tr key={log.id} className="hover:bg-surface-container transition-colors">
753
+ <td className="px-md py-sm text-on-surface-variant text-[12.5px]">{new Date(log.timestamp).toLocaleString()}</td>
754
+ <td className="px-md py-sm font-bold text-on-surface text-[13px]">{log.action}</td>
755
+ <td className="px-md py-sm font-semibold text-on-surface text-[13px]">{log.user_email}</td>
756
+ <td className="px-md py-sm font-semibold text-on-surface-variant text-[13px]">{log.target_name || log.target_id || '-'}</td>
757
+ </tr>
758
+ ))}
759
+ </tbody>
760
+ </table>
761
+ <TablePagination
762
+ currentPage={auditPage}
763
+ totalEntries={auditLogs.length}
764
+ pageSize={auditPageSize}
765
+ onPageChange={setAuditPage}
766
+ onPageSizeChange={setAuditPageSize}
767
+ />
768
+ </>
769
+ )}
770
+ </div>
771
+ </div>
772
+ )}
773
+
774
+ {/* BOOKINGS TAB */}
775
+ {activeTab === 'bookings' && (
776
+ <div className="flex flex-col gap-md mb-xl animate-fade-in">
777
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] mb-md">
778
+ <Calendar className="w-5 h-5 text-primary mr-2" /> Demo Schedule Bookings
779
+ </h2>
780
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto">
781
+ {demoBookings.length === 0 ? (
782
+ <div className="p-xl text-center text-on-surface-variant font-bold">No demo bookings yet.</div>
783
+ ) : (
784
+ <>
785
+ <table className="w-full text-left border-collapse">
786
+ <thead className="bg-surface border-b border-outline-variant text-on-surface-variant">
787
+ <tr>
788
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Client Email</th>
789
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Meeting Date & Time</th>
790
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Status</th>
791
+ </tr>
792
+ </thead>
793
+ <tbody className="divide-y divide-outline-variant">
794
+ {demoBookings.slice((bookingPage - 1) * bookingPageSize, bookingPage * bookingPageSize).map((b) => (
795
+ <tr key={b.id} className="hover:bg-surface-container transition-colors">
796
+ <td className="px-md py-sm font-bold text-on-surface text-[13.5px]">{b.user_email}</td>
797
+ <td className="px-md py-sm text-on-surface-variant text-[13px]">{b.meeting_date} at {b.meeting_time}</td>
798
+ <td className="px-md py-sm">
799
+ <span className={`px-2 py-0.5 rounded text-[11px] font-bold uppercase ${b.status === 'confirmed' ? 'bg-green-500/10 text-green-600' : 'bg-blue-500/10 text-blue-600'}`}>
800
+ {b.status}
801
+ </span>
802
+ </td>
803
+ </tr>
804
+ ))}
805
+ </tbody>
806
+ </table>
807
+ <TablePagination
808
+ currentPage={bookingPage}
809
+ totalEntries={demoBookings.length}
810
+ pageSize={bookingPageSize}
811
+ onPageChange={setBookingPage}
812
+ onPageSizeChange={setBookingPageSize}
813
+ />
814
+ </>
815
+ )}
816
+ </div>
817
+ </div>
818
+ )}
819
+
820
+ {/* EMAILS TAB */}
821
+ {activeTab === 'emails' && (
822
+ <div className="flex flex-col gap-md mb-xl animate-fade-in">
823
+ <h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px] mb-md">
824
+ <Mail className="w-5 h-5 text-primary mr-2" /> Outbound Email Logs
825
+ </h2>
826
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto">
827
+ {emailLogs.length === 0 ? (
828
+ <div className="p-xl text-center text-on-surface-variant font-bold">No email logs available.</div>
829
+ ) : (
830
+ <>
831
+ <table className="w-full text-left border-collapse">
832
+ <thead className="bg-surface border-b border-outline-variant text-on-surface-variant">
833
+ <tr>
834
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Timestamp</th>
835
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Recipient</th>
836
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Subject</th>
837
+ <th className="px-md py-sm font-bold text-[12px] uppercase">Status</th>
838
+ </tr>
839
+ </thead>
840
+ <tbody className="divide-y divide-outline-variant">
841
+ {emailLogs.slice((emailPage - 1) * emailPageSize, emailPage * emailPageSize).map((log) => (
842
+ <tr key={log.id} className="hover:bg-surface-container transition-colors">
843
+ <td className="px-md py-sm text-on-surface-variant text-[12.5px]">{new Date(log.sent_at).toLocaleString()}</td>
844
+ <td className="px-md py-sm font-bold text-on-surface text-[13px]">{log.recipient}</td>
845
+ <td className="px-md py-sm text-on-surface-variant text-[13px]">{log.subject}</td>
846
+ <td className="px-md py-sm">
847
+ <span className={`px-2 py-0.5 rounded text-[11px] font-bold uppercase ${log.status === 'sent' ? 'bg-green-500/10 text-green-600' : 'bg-red-500/10 text-red-600'}`}>
848
+ {log.status}
849
+ </span>
850
+ </td>
851
+ </tr>
852
+ ))}
853
+ </tbody>
854
+ </table>
855
+ <TablePagination
856
+ currentPage={emailPage}
857
+ totalEntries={emailLogs.length}
858
+ pageSize={emailPageSize}
859
+ onPageChange={setEmailPage}
860
+ onPageSizeChange={setEmailPageSize}
861
+ />
862
+ </>
863
+ )}
864
+ </div>
865
+ </div>
866
+ )}
867
+
868
+ {/* Prompt Modal for Editing Role/Org */}
869
+ <CustomModal
870
+ isOpen={promptModal.isOpen}
871
+ onClose={closePrompt}
872
+ title={promptModal.title}
873
+ description={promptModal.desc}
874
+ footer={
875
+ <>
876
+ <button onClick={closePrompt} 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>
877
+ <button onClick={() => promptModal.onConfirm(promptValues)} className="px-4 py-2 bg-primary text-on-primary rounded-lg font-bold border-0 cursor-pointer">Confirm</button>
878
+ </>
879
+ }
880
+ >
881
+ <div className="flex flex-col gap-4">
882
+ {promptModal.inputs.map(input => (
883
+ <div key={input.key} className="flex flex-col">
884
+ <label className="text-[12px] font-bold text-on-surface-variant mb-1">{input.label}</label>
885
+ {input.type === 'select' ? (
886
+ <select
887
+ value={promptValues[input.key] || ''}
888
+ onChange={(e) => handlePromptChange(input.key, e.target.value)}
889
+ className="bg-surface-container border border-outline-variant rounded-lg px-3 py-2 focus:border-primary outline-none text-on-surface"
890
+ >
891
+ {input.options.map(opt => {
892
+ const val = typeof opt === 'object' ? opt.value : opt;
893
+ const lbl = typeof opt === 'object' ? opt.label : opt;
894
+ return <option key={val} value={val}>{lbl}</option>;
895
+ })}
896
+ </select>
897
+ ) : (
898
+ <input
899
+ type={input.type || "text"}
900
+ value={promptValues[input.key] || ''}
901
+ onChange={(e) => handlePromptChange(input.key, e.target.value)}
902
+ placeholder={input.placeholder}
903
+ disabled={input.disabled}
904
+ className={`bg-surface-container border border-outline-variant rounded-lg px-3 py-2 focus:border-primary outline-none text-on-surface ${input.disabled ? 'bg-surface-container-high text-on-surface-variant opacity-80 cursor-not-allowed' : ''}`}
905
+ />
906
+ )}
907
+ </div>
908
+ ))}
909
+ </div>
910
+ </CustomModal>
911
+
912
+ {/* Confirm Modal */}
913
+ <CustomModal
914
+ isOpen={confirmModal.isOpen}
915
+ onClose={closeConfirm}
916
+ title={confirmModal.title}
917
+ description={confirmModal.desc}
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="px-4 py-2 bg-primary text-white rounded-lg font-bold border-0 cursor-pointer">Confirm</button>
922
+ </>
923
+ }
924
+ />
925
+
926
+ </div>
927
+ );
928
+ };
929
+
930
+ export default SupportEngineerPanel;