EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
KnowledgeBase.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4// import removed: TenantSelector
5import './KnowledgeBase.css';
6import { apiFetch } from '../lib/api';
7import { isAdmin } from '../utils/auth';
8
9function KnowledgeBase() {
10 const token = localStorage.getItem('token');
11 let tenantId = '';
12 if (token) {
13 try {
14 const payload = token.split('.')[1];
15 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
16 tenantId = decoded.tenant_id || decoded.tenantId || '';
17 } catch { }
18 }
19 const navigate = useNavigate();
20 const [documents, setDocuments] = useState([]);
21 const [loading, setLoading] = useState(false);
22 const [error, setError] = useState('');
23 const [page, setPage] = useState(1);
24 const [totalPages, setTotalPages] = useState(1);
25 const [tenantMap, setTenantMap] = useState({});
26 const [selectedDoc, setSelectedDoc] = useState(null);
27 const [editorContent, setEditorContent] = useState('');
28 const [deleteConfirm, setDeleteConfirm] = useState(null);
29 const perPage = 10;
30
31 // Optionally fetch tenant map for display, but don't use isRootTenant for column logic
32 useEffect(() => {
33 const fetchTenantMap = async () => {
34 try {
35 const token = localStorage.getItem('token');
36 const res = await apiFetch('/tenants', {
37 headers: { Authorization: `Bearer ${token}` }
38 });
39 if (res.ok) {
40 const data = await res.json();
41 const map = {};
42 data.tenants?.forEach(t => {
43 map[t.tenant_id] = t.name;
44 });
45 setTenantMap(map);
46 }
47 } catch (err) {
48 // Silently fail - 403 is expected for non-MSP users
49 }
50 };
51 fetchTenantMap();
52 }, []);
53
54 const fetchDocuments = async (fetchTenantId = tenantId) => {
55 setLoading(true);
56 try {
57 const token = localStorage.getItem('token');
58 let url = `/documents?page=${page}&limit=${perPage}`;
59 url += `&tenant_id=${tenantId}`;
60 const res = await apiFetch(url, { headers: { Authorization: `Bearer ${token}` } });
61 if (!res.ok) throw new Error('Failed to load documents');
62 const data = await res.json();
63 setDocuments(data.documents);
64 setTotalPages(Math.ceil(data.total / perPage));
65 } catch (err) {
66 console.error(err);
67 setError(err.message);
68 } finally {
69 setLoading(false);
70 }
71 };
72
73 useEffect(() => {
74 fetchDocuments(tenantId);
75 }, [page, tenantId]);
76
77 const handleCreateClick = () => {
78 setSelectedDoc({ title: '', content: '', category: '', doc_id: null });
79 setEditorContent('');
80 };
81
82 const handleEditClick = (docId) => {
83 const doc = documents.find(d => d.doc_id === docId);
84 setSelectedDoc(doc);
85 setEditorContent(doc?.content || '');
86 };
87
88 const handleSave = async () => {
89 setLoading(true);
90 try {
91 const token = localStorage.getItem('token');
92 const method = selectedDoc.doc_id ? 'PUT' : 'POST';
93 const url = selectedDoc.doc_id ? `/documents/${selectedDoc.doc_id}` : '/documents';
94 const res = await apiFetch(url, {
95 method,
96 headers: {
97 'Content-Type': 'application/json',
98 Authorization: `Bearer ${token}`
99 },
100 body: JSON.stringify({ ...selectedDoc, content: editorContent, tenant_id: tenantId })
101 });
102 if (!res.ok) throw new Error('Failed to save document');
103 setSelectedDoc(null);
104 fetchDocuments(tenantId);
105 } catch (err) {
106 setError(err.message);
107 } finally {
108 setLoading(false);
109 }
110 };
111
112 const handleDelete = async (docId) => {
113 setLoading(true);
114 try {
115 const token = localStorage.getItem('token');
116 const res = await apiFetch(`/documents/${docId}`, {
117 method: 'DELETE',
118 headers: { Authorization: `Bearer ${token}` }
119 });
120 if (!res.ok) throw new Error('Failed to delete document');
121 setDeleteConfirm(null);
122 fetchDocuments(tenantId);
123 } catch (err) {
124 setError(err.message);
125 } finally {
126 setLoading(false);
127 }
128 };
129
130 const handleDeleteClick = (doc) => {
131 setDeleteConfirm(doc);
132 };
133
134 // Show tenant column if any document has tenant_id
135 const showTenantColumn = documents.some(doc => doc.tenant_id !== undefined && doc.tenant_id !== null);
136
137 return (
138 <MainLayout>
139 <div className="page-content">
140 <div className="page-header">
141 <h2>Documentation</h2>
142 <button className="btn" onClick={handleCreateClick}>
143 <span className="material-symbols-outlined">add</span>
144 Create Document
145 </button>
146 </div>
147 {error && <div className="error-message">{error}</div>}
148 {loading ? (
149 <div className="loading">Loading documents...</div>
150 ) : selectedDoc ? (
151 <div className="editor-container">
152 <input
153 type="text"
154 value={selectedDoc.title}
155 onChange={e => setSelectedDoc({ ...selectedDoc, title: e.target.value })}
156 placeholder="Title"
157 style={{ width: '100%', marginBottom: 8, padding: 8 }}
158 />
159 <input
160 type="text"
161 value={selectedDoc.category}
162 onChange={e => setSelectedDoc({ ...selectedDoc, category: e.target.value })}
163 placeholder="Category"
164 style={{ width: '100%', marginBottom: 8, padding: 8 }}
165 />
166 <textarea
167 value={editorContent}
168 onChange={e => setEditorContent(e.target.value)}
169 placeholder="Document content..."
170 style={{ width: '100%', minHeight: 300, marginBottom: 16, padding: 8, borderRadius: 4, background: '#fff', color: '#222' }}
171 />
172 <div>
173 <button className="btn" onClick={handleSave} style={{ marginRight: 8 }}>Save</button>
174 <button className="btn" onClick={() => setSelectedDoc(null)}>Cancel</button>
175 </div>
176 </div>
177 ) : (
178 <>
179 <div className="table-container">
180 <table className="data-table">
181 <thead>
182 <tr>
183 <th>ID</th>
184 <th>Title</th>
185 {showTenantColumn && <th>Tenant</th>}
186 <th>Category</th>
187 <th>Last Updated</th>
188 <th>Author</th>
189 <th>Actions</th>
190 </tr>
191 </thead>
192 <tbody>
193 {documents.map(doc => (
194 <tr key={doc.doc_id}>
195 <td>#{doc.doc_id}</td>
196 <td>{doc.title}</td>
197 {showTenantColumn && (
198 <td>{doc.tenant_id === 1
199 ? 'Root Tenant'
200 : (doc.tenant_name || tenantMap[doc.tenant_id] || '-')}</td>
201 )}
202 <td>{doc.category}</td>
203 <td>{new Date(doc.updated_at).toLocaleDateString()}</td>
204 <td>{doc.author_name}</td>
205 <td>
206 <button
207 className="btn"
208 onClick={() => handleEditClick(doc.doc_id)}
209 style={{ marginRight: '8px' }}
210 >
211 <span className="material-symbols-outlined">edit</span>
212 </button>
213 {isAdmin() && (
214 <button
215 className="btn danger"
216 onClick={() => handleDeleteClick(doc)}
217 >
218 <span className="material-symbols-outlined">delete</span>
219 </button>
220 )}
221 </td>
222 </tr>
223 ))}
224 </tbody>
225 </table>
226 </div>
227 <div className="pagination">
228 <button className="btn" disabled={page === 1} onClick={() => setPage(p => p - 1)}>
229 <span className="material-symbols-outlined">navigate_before</span>
230 </button>
231 <span>
232 Page {page} of {totalPages}
233 </span>
234 <button className="btn" disabled={page === totalPages} onClick={() => setPage(p => p + 1)}>
235 <span className="material-symbols-outlined">navigate_next</span>
236 </button>
237 </div>
238 </>
239 )}
240
241 {deleteConfirm && (
242 <div className="modal-overlay" onClick={() => setDeleteConfirm(null)}>
243 <div className="modal-content" onClick={e => e.stopPropagation()}>
244 <div className="modal-header">
245 <h3>Confirm Delete</h3>
246 <button
247 className="btn"
248 onClick={() => setDeleteConfirm(null)}
249 style={{ padding: '4px', minWidth: 'auto' }}
250 >
251 <span className="material-symbols-outlined">close</span>
252 </button>
253 </div>
254 <div className="modal-body">
255 <p>Are you sure you want to delete the document:</p>
256 <p><strong>"{deleteConfirm.title}"</strong></p>
257 <p style={{ color: 'var(--error)', fontSize: '0.9em' }}>
258 This action cannot be undone.
259 </p>
260 </div>
261 <div className="modal-footer">
262 <button
263 className="btn"
264 onClick={() => setDeleteConfirm(null)}
265 style={{ marginRight: '8px' }}
266 >
267 Cancel
268 </button>
269 <button
270 className="btn danger"
271 onClick={() => handleDelete(deleteConfirm.doc_id)}
272 >
273 Delete
274 </button>
275 </div>
276 </div>
277 </div>
278 )}
279 </div>
280 </MainLayout>
281 );
282}
283
284export default KnowledgeBase;