1import { useState, useEffect, useRef } from 'react';
2import Quill from 'quill';
3import { useNavigate, useParams } from 'react-router-dom';
4import MainLayout from '../components/Layout/MainLayout';
5// import removed: TenantSelector
6import AIRewordButton from '../components/AIRewordButton';
7import './DocumentForm.css';
8import { apiFetch } from '../lib/api';
10function DocumentForm() {
11 const navigate = useNavigate();
12 const { id } = useParams();
13 const [selectedTenantId, setSelectedTenantId] = useState('');
14 const [form, setForm] = useState({ title: '', content: '', category: '' });
15 const [loading, setLoading] = useState(false);
16 const [loadingData, setLoadingData] = useState(false);
17 const [error, setError] = useState('');
18 const [attachments, setAttachments] = useState([]);
19 const quillRefGlobal = useRef(null);
27 // Initialize Quill editor once on mount
29 if (!quillRefGlobal.current) {
30 const editor = document.getElementById('quill-editor');
32 const quill = new Quill(editor, { theme: 'snow' });
33 quill.enable(false); // disable until data is loaded
34 quill.on('text-change', () => {
35 setForm(f => ({ ...f, content: quill.root.innerHTML }));
37 quillRefGlobal.current = quill;
38 console.log('[Quill] Editor initialized');
40 console.error('[Quill] #quill-editor not found in DOM!');
45 const getTenantHeaders = async (baseHeaders = {}) => {
46 const headers = { ...baseHeaders };
47 if (selectedTenantId) {
48 const token = localStorage.getItem('token');
49 const tenantRes = await apiFetch('/tenants', {
51 'Authorization': `Bearer ${token}`,
52 'X-Tenant-Subdomain': 'admin'
56 const data = await tenantRes.json();
57 const tenants = Array.isArray(data) ? data : (data.tenants || []);
58 const tenant = tenants.find(t => t.tenant_id === parseInt(selectedTenantId));
60 headers['X-Tenant-Subdomain'] = tenant.subdomain;
67 // Sync Quill with fetched content
69 if (quillRefGlobal.current && form.content !== undefined) {
70 const quill = quillRefGlobal.current;
71 const currentHtml = quill.root.innerHTML;
72 if ((form.content || '') !== currentHtml) {
73 console.log('[Quill] Syncing fetched content');
74 quill.setContents([]);
75 quill.clipboard.dangerouslyPasteHTML(form.content || '');
81 const fetchDocument = async () => {
84 const token = localStorage.getItem('token');
85 const res = await apiFetch(`/documents/${id}`, {
86 headers: { Authorization: `Bearer ${token}` }
88 if (!res.ok) throw new Error('Failed to load document');
89 const data = await res.json();
91 if (data.tenant_id && !selectedTenantId) {
92 setSelectedTenantId(String(data.tenant_id));
96 title: data.title || '',
97 content: data.content || '',
98 category: data.category || '',
99 attachments: data.attachments || []
102 // Apply content to Quill once loaded
103 if (quillRefGlobal.current && data.content) {
104 quillRefGlobal.current.clipboard.dangerouslyPasteHTML(data.content);
105 quillRefGlobal.current.enable(true);
108 setError(err.message || 'Error loading document');
110 setLoadingData(false);
114 const handleSubmit = async (e) => {
119 const token = localStorage.getItem('token');
120 const payload = { title: form.title, content: form.content, category: form.category };
121 const url = id ? `/documents/${id}` : '/documents';
122 const method = id ? 'PUT' : 'POST';
123 const headers = await getTenantHeaders({
124 'Content-Type': 'application/json',
125 Authorization: `Bearer ${token}`
128 if (!id && attachments.length) {
129 payload.attachments = attachments;
132 const res = await apiFetch(url, {
135 body: JSON.stringify(payload),
138 if (!res.ok) throw new Error(`Failed to ${id ? 'update' : 'create'} document`);
139 navigate('/knowledge-base');
142 setError(err.message || 'Save failed');
148 const handleFileChange = (ev) => {
149 const file = ev.target.files && ev.target.files[0];
151 const reader = new FileReader();
152 reader.onload = () => {
153 const data = reader.result.split(',')[1];
154 setAttachments([{ filename: file.name, data }]);
156 reader.readAsDataURL(file);
159 const handleUploadAttachment = async () => {
160 if (!id || attachments.length === 0) {
161 setError('Please select a file to upload');
167 const token = localStorage.getItem('token');
168 const headers = await getTenantHeaders({
169 'Content-Type': 'application/json',
170 Authorization: `Bearer ${token}`
173 const res = await apiFetch(`/documents/${id}/attachments`, {
176 body: JSON.stringify({ attachments }),
180 const errData = await res.json().catch(() => ({}));
181 throw new Error(errData.error || 'Failed to upload attachment');
184 await fetchDocument();
186 const fileInput = document.getElementById('attachment');
187 if (fileInput) fileInput.value = '';
190 setError(err.message || 'Upload failed');
198 <div className="page-content">
199 <div className="page-header">
200 <h2>{id ? 'Edit Document' : 'Create Document'}</h2>
201 <div className="header-actions">
202 <button className="btn btn-secondary" onClick={() => navigate('/knowledge-base')}>
203 <span className="material-symbols-outlined">arrow_back</span>
209 {error && <div className="alert alert-error">{error}</div>}
213 <div className="loading-state">
214 <span className="material-symbols-outlined spinning">progress_activity</span>
215 <p>Loading document...</p>
218 <form onSubmit={handleSubmit} className="document-form-container">
219 <div className="form-card">
220 <div className="card-header">
222 <span className="material-symbols-outlined">description</span>
226 <div className="card-body">
227 <div className="form-group" style={{ position: 'relative' }}>
228 <label htmlFor="title">Title *</label>
232 placeholder="Enter document title"
234 onChange={(e) => setForm({ ...form, title: e.target.value })}
236 style={{ paddingRight: 40 }}
240 onReword={(reworded) => setForm(f => ({ ...f, title: reworded }))}
241 disabled={!form.title}
242 style={{ position: 'absolute', right: 8, top: 8 }}
245 <div className="form-group">
246 <label htmlFor="category">Category</label>
250 placeholder="e.g., Policy, Guide, Report"
251 value={form.category}
252 onChange={(e) => setForm({ ...form, category: e.target.value })}
258 <div className="form-card">
259 <div className="card-header">
261 <span className="material-symbols-outlined">edit_note</span>
265 <div className="card-body">
267 <div>Loading content...</div>
269 <div className="quill-wrapper" style={{ position: 'relative' }}>
270 <div id="quill-editor" />
273 onReword={(reworded) => {
274 setForm(f => ({ ...f, content: reworded }));
275 if (quillRefGlobal.current) {
276 quillRefGlobal.current.clipboard.dangerouslyPasteHTML(reworded);
279 disabled={!form.content}
280 style={{ position: 'absolute', right: 8, top: 8, zIndex: 10 }}
287 <div className="form-card">
288 <div className="card-header">
290 <span className="material-symbols-outlined">attach_file</span>
294 <div className="card-body">
295 {Array.isArray(form.attachments) && form.attachments.length > 0 && (
296 <div className="existing-attachments">
297 <label>Existing Files</label>
298 <ul className="attachment-list" style={{ listStyle: 'none', padding: 0 }}>
299 {form.attachments.map(a => (
300 <li key={a.id} className="attachment-item" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem', borderBottom: '1px solid #e0e0e0' }}>
301 <span className="material-symbols-outlined" style={{ fontSize: '20px', color: '#666' }}>insert_drive_file</span>
302 <span style={{ flex: 1 }}>{a.original_filename || a.filename}</span>
305 style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', borderRadius: '4px', border: '1px solid #ccc', backgroundColor: '#fff', cursor: 'pointer', textDecoration: 'none', color: '#333' }}
306 href={`/documents/${id}/attachments/${a.id}/download`}
310 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>download</span>
318 <div className="form-group" style={{ marginTop: form.attachments?.length ? '1rem' : 0 }}>
319 <label htmlFor="attachment">{id ? 'Add Another File' : 'Upload File (optional)'}</label>
320 <input id="attachment" type="file" onChange={handleFileChange} className="file-input" />
321 {attachments.length > 0 && (
322 <div className="file-selected" style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
323 <span className="material-symbols-outlined" style={{ color: 'green' }}>check_circle</span>
324 <span>Selected: {attachments[0].filename}</span>
327 {id && attachments.length > 0 && (
328 <button type="button" className="btn btn-primary" style={{ marginTop: '0.75rem' }} onClick={handleUploadAttachment} disabled={loading}>
329 <span className="material-symbols-outlined">upload</span>
330 {loading ? 'Uploading...' : 'Upload Document'}
337 <div className="form-actions">
338 <button type="submit" className="btn btn-primary" disabled={loading || !form.title}>
341 <span className="material-symbols-outlined spinning">progress_activity</span>
346 <span className="material-symbols-outlined">save</span>
347 {id ? 'Update Document' : 'Create Document'}
351 <button type="button" className="btn btn-secondary" onClick={() => navigate('/knowledge-base')} disabled={loading}>
352 <span className="material-symbols-outlined">close</span>
363export default DocumentForm;