EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TenantDetail.jsx
Go to the documentation of this file.
1import { useEffect, useState } from 'react';
2import { useParams, useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4import { apiFetch } from '../lib/api';
5import { notifySuccess, notifyError } from '../utils/notifications';
6import './Customers.css';
7import './Settings.css';
8
9
10function TenantDetail() {
11 const { id } = useParams();
12 const navigate = useNavigate();
13 const [data, setData] = useState(null);
14 const [loading, setLoading] = useState(false);
15 const [error, setError] = useState('');
16
17 // Create user modal state
18 const [showCreateUserModal, setShowCreateUserModal] = useState(false);
19 const [newUser, setNewUser] = useState({
20 name: '',
21 email: '',
22 role: 'staff',
23 password: ''
24 });
25 const [creatingUser, setCreatingUser] = useState(false);
26
27 // Users list and editing state
28 const [users, setUsers] = useState([]);
29 const [usersLoading, setUsersLoading] = useState(false);
30 const [editingUser, setEditingUser] = useState(null);
31 const [editUserData, setEditUserData] = useState({
32 name: '',
33 email: '',
34 role: ''
35 });
36 const [updatingUser, setUpdatingUser] = useState(false);
37
38 useEffect(() => {
39 fetchDetail();
40 fetchUsers();
41 }, [id]);
42
43 async function fetchDetail() {
44 setLoading(true);
45 setError('');
46 try {
47 const token = localStorage.getItem('token');
48 const res = await apiFetch(`/tenants/${id}`, {
49 headers: {
50 Authorization: `Bearer ${token}`,
51 'X-Tenant-Subdomain': 'admin'
52 }
53 });
54
55 if (!res.ok) throw new Error('Failed to load tenant');
56 const json = await res.json();
57 setData(json);
58 } catch (err) {
59 console.error(err);
60 setError(err.message || 'Load error');
61 } finally {
62 setLoading(false);
63 }
64 }
65
66 async function fetchUsers() {
67 setUsersLoading(true);
68 try {
69 const token = localStorage.getItem('token');
70 const res = await apiFetch(`/users?tenant_id=${id}`, {
71 headers: {
72 Authorization: `Bearer ${token}`
73 }
74 });
75
76 if (!res.ok) throw new Error('Failed to load users');
77 const usersData = await res.json();
78 // Filter to only show users from this tenant
79 setUsers(usersData.filter(u => u.tenant_id === id));
80 } catch (err) {
81 console.error('Error fetching users:', err);
82 setUsers([]);
83 } finally {
84 setUsersLoading(false);
85 }
86 }
87
88 const handleCreateUser = async () => {
89 if (!newUser.name || !newUser.email || !newUser.password) {
90 notifyError('Please fill in all required fields');
91 return;
92 }
93
94 setCreatingUser(true);
95 try {
96 const token = localStorage.getItem('token');
97 const res = await apiFetch('/users', {
98 method: 'POST',
99 headers: {
100 'Content-Type': 'application/json',
101 Authorization: `Bearer ${token}`
102 },
103 body: JSON.stringify({
104 ...newUser,
105 tenant_id: id // Use the current tenant ID
106 })
107 });
108
109 if (!res.ok) {
110 const errData = await res.json();
111 throw new Error(errData.error || 'Failed to create user');
112 }
113
114 notifySuccess('User created successfully');
115 setShowCreateUserModal(false);
116 setNewUser({ name: '', email: '', role: 'staff', password: '' });
117
118 // Refresh users and tenant stats
119 fetchUsers();
120 fetchDetail();
121 } catch (err) {
122 console.error(err);
123 notifyError(err.message || 'Failed to create user');
124 } finally {
125 setCreatingUser(false);
126 }
127 };
128
129 const startEditUser = (user) => {
130 setEditingUser(user);
131 setEditUserData({
132 name: user.name,
133 email: user.email,
134 role: user.role
135 });
136 };
137
138 const cancelEditUser = () => {
139 setEditingUser(null);
140 setEditUserData({ name: '', email: '', role: '' });
141 };
142
143 const updateUser = async () => {
144 if (!editingUser || !editUserData.name || !editUserData.email) {
145 notifyError('Please fill in all required fields');
146 return;
147 }
148
149 setUpdatingUser(true);
150 try {
151 const token = localStorage.getItem('token');
152 const res = await apiFetch(`/users/${editingUser.user_id}`, {
153 method: 'PUT',
154 headers: {
155 'Content-Type': 'application/json',
156 Authorization: `Bearer ${token}`
157 },
158 body: JSON.stringify({
159 name: editUserData.name,
160 email: editUserData.email,
161 role: editUserData.role
162 })
163 });
164
165 if (!res.ok) {
166 const errorData = await res.json();
167 throw new Error(errorData.error || 'Update failed');
168 }
169
170 notifySuccess('User updated successfully');
171 setEditingUser(null);
172 setEditUserData({ name: '', email: '', role: '' });
173
174 // Refresh users list
175 fetchUsers();
176 } catch (err) {
177 console.error(err);
178 notifyError(err.message || 'Failed to update user');
179 } finally {
180 setUpdatingUser(false);
181 }
182 };
183
184 const deleteUser = async (user) => {
185 if (!window.confirm(`Are you sure you want to delete user "${user.name}"?`)) return;
186
187 try {
188 const token = localStorage.getItem('token');
189 const res = await apiFetch(`/users/${user.user_id}`, {
190 method: 'DELETE',
191 headers: {
192 Authorization: `Bearer ${token}`
193 }
194 });
195
196 if (!res.ok) throw new Error('Delete failed');
197
198 notifySuccess('User deleted successfully');
199
200 // Refresh users and stats
201 fetchUsers();
202 fetchDetail();
203 } catch (err) {
204 console.error(err);
205 notifyError(err.message || 'Failed to delete user');
206 }
207 };
208
209 if (loading) return (
210 <MainLayout>
211 <div className="page-content"><div className="loading">Loading tenant...</div></div>
212 </MainLayout>
213 );
214
215 if (error) return (
216 <MainLayout>
217 <div className="page-content"><div className="error-message">{error}</div></div>
218 </MainLayout>
219 );
220
221 if (!data) return null;
222
223 const { tenant, stats } = data;
224
225 return (
226 <MainLayout>
227 <div className="page-content">
228 <div className="page-header">
229 <h2>{tenant.name}</h2>
230 <div style={{ display: 'flex', gap: '8px' }}>
231 <button className="btn" onClick={() => navigate(`/tenants/${id}/edit`)}>
232 <span className="material-symbols-outlined">edit</span> Edit
233 </button>
234 </div>
235 </div>
236
237 <section className="dashboard-grid">
238 {/* Tenant Information Card */}
239 <div className="dashboard-card">
240 <h3>Tenant Information</h3>
241 <div style={{ display: 'grid', gap: '12px' }}>
242 <div>
243 <strong>Status:</strong>{' '}
244 <span className={`status-badge status-${tenant.status?.toLowerCase() || 'active'}`}>
245 {tenant.status || 'Active'}
246 </span>
247 </div>
248 <div>
249 <strong>Type:</strong>{' '}
250 {tenant.is_msp ? (
251 <span style={{
252 padding: '4px 8px',
253 borderRadius: '4px',
254 backgroundColor: 'var(--primary)',
255 color: 'white',
256 fontSize: '0.9em',
257 fontWeight: 'bold'
258 }}>
259 ROOT TENANT
260 </span>
261 ) : (
262 <span style={{
263 padding: '4px 8px',
264 borderRadius: '4px',
265 backgroundColor: 'var(--bg-secondary)',
266 color: 'var(--text-muted)',
267 fontSize: '0.9em'
268 }}>
269 Client Tenant
270 </span>
271 )}
272 </div>
273 <div>
274 <strong>Subdomain:</strong> <code>{tenant.subdomain}</code>
275 </div>
276 <div>
277 <strong>Tenant ID:</strong> <code style={{ fontSize: '0.85em' }}>{tenant.tenant_id}</code>
278 </div>
279 <div>
280 <strong>Created:</strong> {new Date(tenant.created_at).toLocaleString()}
281 </div>
282 <div>
283 <strong>Last Updated:</strong> {new Date(tenant.updated_at).toLocaleString()}
284 </div>
285 </div>
286 </div>
287
288 {/* Statistics Card */}
289 <div className="dashboard-card">
290 <h3>Statistics</h3>
291 <div style={{ display: 'grid', gap: '16px' }}>
292 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
293 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
294 <span className="material-symbols-outlined" style={{ color: 'var(--primary)' }}>group</span>
295 <span>Users</span>
296 </div>
297 <strong style={{ fontSize: '1.2em' }}>{stats.user_count || 0}</strong>
298 </div>
299 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
300 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
301 <span className="material-symbols-outlined" style={{ color: 'var(--success)' }}>business</span>
302 <span>Customers</span>
303 </div>
304 <strong style={{ fontSize: '1.2em' }}>{stats.customer_count || 0}</strong>
305 </div>
306 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
307 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
308 <span className="material-symbols-outlined" style={{ color: 'var(--warning)' }}>confirmation_number</span>
309 <span>Tickets</span>
310 </div>
311 <strong style={{ fontSize: '1.2em' }}>{stats.ticket_count || 0}</strong>
312 </div>
313 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
314 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
315 <span className="material-symbols-outlined" style={{ color: 'var(--info)' }}>receipt</span>
316 <span>Invoices</span>
317 </div>
318 <strong style={{ fontSize: '1.2em' }}>{stats.invoice_count || 0}</strong>
319 </div>
320 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
321 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
322 <span className="material-symbols-outlined" style={{ color: 'var(--secondary)' }}>description</span>
323 <span>Contracts</span>
324 </div>
325 <strong style={{ fontSize: '1.2em' }}>{stats.contract_count || 0}</strong>
326 </div>
327 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
328 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
329 <span className="material-symbols-outlined" style={{ color: 'var(--text-muted)' }}>computer</span>
330 <span>Agents</span>
331 </div>
332 <strong style={{ fontSize: '1.2em' }}>{stats.agent_count || 0}</strong>
333 </div>
334 </div>
335 </div>
336
337 {/* Access Information Card */}
338 <div className="dashboard-card">
339 <h3>Access Information</h3>
340 <div style={{ display: 'grid', gap: '12px' }}>
341 <div>
342 <strong>Portal URL:</strong>
343 <div style={{
344 marginTop: '4px',
345 padding: '8px 12px',
346 backgroundColor: 'var(--bg-secondary)',
347 borderRadius: '4px',
348 fontFamily: 'monospace'
349 }}>
350 https://{tenant.subdomain}.yourdomain.com
351 </div>
352 </div>
353 <div style={{
354 padding: '12px',
355 borderRadius: '4px',
356 backgroundColor: 'var(--bg-secondary)',
357 border: '1px solid var(--border)'
358 }}>
359 <div style={{ fontSize: '0.9em', color: 'var(--text-muted)' }}>
360 <strong>Development Access:</strong>
361 <div style={{ marginTop: '8px' }}>
362 Add header to requests:
363 <code style={{
364 display: 'block',
365 marginTop: '4px',
366 padding: '4px 8px',
367 backgroundColor: 'var(--bg-primary)',
368 borderRadius: '2px'
369 }}>
370 X-Tenant-Subdomain: {tenant.subdomain}
371 </code>
372 </div>
373 </div>
374 </div>
375 </div>
376 </div>
377
378 {/* Quick Actions Card */}
379 <div className="dashboard-card">
380 <h3>Quick Actions</h3>
381 <div style={{ display: 'grid', gap: '8px' }}>
382 <button
383 className="btn"
384 onClick={() => setShowCreateUserModal(true)}
385 style={{ justifyContent: 'flex-start' }}
386 >
387 <span className="material-symbols-outlined">person_add</span>
388 Create User for This Tenant
389 </button>
390 <button
391 className="btn"
392 onClick={() => navigate(`/tenants/${id}/audit`)}
393 style={{ justifyContent: 'flex-start' }}
394 >
395 <span className="material-symbols-outlined">history</span>
396 View Audit Log
397 </button>
398 <button
399 className="btn"
400 onClick={() => navigate('/tenants')}
401 style={{ justifyContent: 'flex-start' }}
402 >
403 <span className="material-symbols-outlined">arrow_back</span>
404 Back to Tenants List
405 </button>
406 </div>
407 </div>
408 </section>
409
410 {/* Users Section */}
411 <section style={{ marginTop: '2rem' }}>
412 <div className="dashboard-card">
413 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
414 <h3 style={{ margin: 0 }}>Users ({users.length})</h3>
415 <button
416 className="btn primary"
417 onClick={() => setShowCreateUserModal(true)}
418 style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
419 >
420 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>add</span>
421 Add User
422 </button>
423 </div>
424
425 {usersLoading ? (
426 <div className="loading">Loading users...</div>
427 ) : users.length === 0 ? (
428 <div style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-muted)' }}>
429 <span className="material-symbols-outlined" style={{ fontSize: '48px', opacity: 0.3 }}>group_off</span>
430 <p>No users found for this tenant.</p>
431 <button className="btn primary" onClick={() => setShowCreateUserModal(true)}>
432 Create First User
433 </button>
434 </div>
435 ) : (
436 <div style={{ overflowX: 'auto' }}>
437 <table className="data-table">
438 <thead>
439 <tr>
440 <th>Name</th>
441 <th>Email</th>
442 <th>Role</th>
443 <th>Actions</th>
444 </tr>
445 </thead>
446 <tbody>
447 {users.map(u => (
448 <tr key={u.user_id}>
449 <td>{u.name}</td>
450 <td>{u.email}</td>
451 <td>
452 <span className={`status-badge status-${u.role}`}>
453 {u.role}
454 </span>
455 </td>
456 <td>
457 <div className="action-buttons">
458 <button
459 className="btn primary"
460 onClick={() => startEditUser(u)}
461 style={{ fontSize: '0.9rem', padding: '0.4rem 0.8rem' }}
462 >
463 Edit
464 </button>
465 <button
466 className="btn"
467 onClick={() => deleteUser(u)}
468 style={{ fontSize: '0.9rem', padding: '0.4rem 0.8rem' }}
469 >
470 Delete
471 </button>
472 </div>
473 </td>
474 </tr>
475 ))}
476 </tbody>
477 </table>
478 </div>
479 )}
480 </div>
481 </section>
482
483 {/* Edit User Modal */}
484 {editingUser && (
485 <div className="edit-user-modal">
486 <div className="edit-user-modal-content">
487 <h3>Edit User: {editingUser.name}</h3>
488
489 <div className="form-group">
490 <label>Name *</label>
491 <input
492 type="text"
493 value={editUserData.name}
494 onChange={(e) => setEditUserData({ ...editUserData, name: e.target.value })}
495 placeholder="Full name"
496 />
497 </div>
498
499 <div className="form-group">
500 <label>Email *</label>
501 <input
502 type="email"
503 value={editUserData.email}
504 onChange={(e) => setEditUserData({ ...editUserData, email: e.target.value })}
505 placeholder="user@example.com"
506 />
507 </div>
508
509 <div className="form-group">
510 <label>Role</label>
511 <select
512 value={editUserData.role}
513 onChange={(e) => setEditUserData({ ...editUserData, role: e.target.value })}
514 >
515 <option value="staff">Staff</option>
516 <option value="admin">Admin</option>
517 <option value="msp">MSP</option>
518 </select>
519 </div>
520
521 <div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
522 <button
523 className="btn primary"
524 onClick={updateUser}
525 disabled={updatingUser}
526 >
527 {updatingUser ? 'Updating...' : 'Save Changes'}
528 </button>
529 <button
530 className="btn"
531 onClick={cancelEditUser}
532 disabled={updatingUser}
533 >
534 Cancel
535 </button>
536 </div>
537 </div>
538 </div>
539 )}
540
541 {/* Create User Modal */}
542 {showCreateUserModal && (
543 <div className="edit-user-modal">
544 <div className="edit-user-modal-content">
545 <h3>Create User for {tenant.name}</h3>
546
547 <div className="form-group">
548 <label>Name *</label>
549 <input
550 type="text"
551 value={newUser.name}
552 onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
553 placeholder="Full name"
554 />
555 </div>
556
557 <div className="form-group">
558 <label>Email *</label>
559 <input
560 type="email"
561 value={newUser.email}
562 onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
563 placeholder="user@example.com"
564 />
565 </div>
566
567 <div className="form-group">
568 <label>Password *</label>
569 <input
570 type="password"
571 value={newUser.password}
572 onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
573 placeholder="Enter password"
574 />
575 </div>
576
577 <div className="form-group">
578 <label>Role</label>
579 <select
580 value={newUser.role}
581 onChange={(e) => setNewUser({ ...newUser, role: e.target.value })}
582 >
583 <option value="staff">Staff</option>
584 <option value="admin">Admin</option>
585 </select>
586 </div>
587
588 <div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
589 <button
590 className="btn primary"
591 onClick={handleCreateUser}
592 disabled={creatingUser}
593 >
594 {creatingUser ? 'Creating...' : 'Create User'}
595 </button>
596 <button
597 className="btn"
598 onClick={() => {
599 setShowCreateUserModal(false);
600 setNewUser({ name: '', email: '', role: 'staff', password: '' });
601 }}
602 disabled={creatingUser}
603 >
604 Cancel
605 </button>
606 </div>
607 </div>
608 </div>
609 )}
610 </div>
611 </MainLayout>
612 );
613}
614
615export default TenantDetail;