EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TenantAudit.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 './Customers.css';
6
7export default function TenantAudit() {
8 const { id } = useParams();
9 const navigate = useNavigate();
10 const [logs, setLogs] = useState([]);
11 const [tenant, setTenant] = useState(null);
12 const [total, setTotal] = useState(0);
13 const [page, setPage] = useState(1);
14 const [limit] = useState(50);
15 const [loading, setLoading] = useState(true);
16 const [error, setError] = useState('');
17
18 useEffect(() => {
19 fetchTenant();
20 fetchAuditLogs();
21 }, [id, page]);
22
23 async function fetchTenant() {
24 try {
25 const token = localStorage.getItem('token');
26 const res = await apiFetch(`/tenants/${id}`, {
27 headers: {
28 Authorization: `Bearer ${token}`,
29 'X-Tenant-Subdomain': 'admin'
30 }
31 });
32
33 if (res.ok) {
34 const json = await res.json();
35 setTenant(json.tenant);
36 }
37 } catch (err) {
38 console.error('Failed to load tenant:', err);
39 }
40 }
41
42 async function fetchAuditLogs() {
43 setLoading(true);
44 setError('');
45 try {
46 const token = localStorage.getItem('token');
47 const params = new URLSearchParams();
48 params.set('page', page);
49 params.set('limit', limit);
50
51 const res = await apiFetch(`/tenants/${id}/audit?${params.toString()}`, {
52 headers: {
53 Authorization: `Bearer ${token}`,
54 'X-Tenant-Subdomain': 'admin'
55 }
56 });
57
58 if (!res.ok) {
59 const text = await res.text();
60 throw new Error(text || 'Failed to load audit logs');
61 }
62
63 const json = await res.json();
64 setLogs(json.logs || []);
65 setTotal(json.total || 0);
66 } catch (err) {
67 console.error('Audit log error:', err);
68 setError(err.message || 'Failed to load audit logs');
69 } finally {
70 setLoading(false);
71 }
72 }
73
74 function formatDate(dateString) {
75 if (!dateString) return '-';
76 const date = new Date(dateString);
77 return date.toLocaleString('en-AU', {
78 year: 'numeric',
79 month: 'short',
80 day: 'numeric',
81 hour: '2-digit',
82 minute: '2-digit'
83 });
84 }
85
86 function getActionBadgeClass(action) {
87 const actionLower = action?.toLowerCase() || '';
88 if (actionLower.includes('create')) return 'status-active';
89 if (actionLower.includes('update') || actionLower.includes('edit')) return 'status-pending';
90 if (actionLower.includes('delete')) return 'status-closed';
91 return 'status-draft';
92 }
93
94 const totalPages = Math.ceil(total / limit);
95
96 return (
97 <MainLayout>
98 <div className="page-content">
99 <div className="page-header">
100 <div className="header-content">
101 <h2>
102 {tenant ? `${tenant.name} - Audit Log` : 'Tenant Audit Log'}
103 </h2>
104 </div>
105 <div className="header-actions">
106 <button className="btn btn-secondary" onClick={() => navigate(`/tenants/${id}`)}>
107 <span className="material-symbols-outlined">arrow_back</span>
108 Back to Tenant
109 </button>
110 </div>
111 </div>
112
113 {error && <div className="error-message">{error}</div>}
114
115 {loading ? (
116 <div className="loading">Loading audit logs...</div>
117 ) : logs.length === 0 ? (
118 <div className="empty-state">
119 <span className="material-symbols-outlined">history</span>
120 <p>No audit logs found for this tenant</p>
121 </div>
122 ) : (
123 <>
124 <table className="data-table">
125 <thead>
126 <tr>
127 <th>Timestamp</th>
128 <th>User</th>
129 <th>Action</th>
130 <th>Resource</th>
131 <th>Details</th>
132 </tr>
133 </thead>
134 <tbody>
135 {logs.map((log, idx) => (
136 <tr key={log.audit_id || idx}>
137 <td style={{ whiteSpace: 'nowrap' }}>
138 {formatDate(log.created_at)}
139 </td>
140 <td>
141 {log.user_name || log.user_email || `User #${log.user_id}` || '-'}
142 </td>
143 <td>
144 <span className={`status-badge ${getActionBadgeClass(log.action)}`}>
145 {log.action || '-'}
146 </span>
147 </td>
148 <td>
149 {log.resource_type || '-'}
150 {log.resource_id && (
151 <span style={{ color: 'var(--text-muted)', fontSize: '0.9em' }}>
152 {' '}#{log.resource_id}
153 </span>
154 )}
155 </td>
156 <td style={{ maxWidth: '400px', wordWrap: 'break-word' }}>
157 {log.details ? (
158 typeof log.details === 'string' ? log.details : JSON.stringify(log.details)
159 ) : '-'}
160 </td>
161 </tr>
162 ))}
163 </tbody>
164 </table>
165
166 {totalPages > 1 && (
167 <div className="pagination">
168 <button
169 className="btn"
170 disabled={page <= 1}
171 onClick={() => setPage(p => Math.max(1, p - 1))}
172 >
173 <span className="material-symbols-outlined">navigate_before</span>
174 </button>
175 <span>Page {page} of {totalPages}</span>
176 <button
177 className="btn"
178 disabled={page >= totalPages}
179 onClick={() => setPage(p => p + 1)}
180 >
181 <span className="material-symbols-outlined">navigate_next</span>
182 </button>
183 </div>
184 )}
185 </>
186 )}
187 </div>
188 </MainLayout>
189 );
190}