EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
Settings.jsx
Go to the documentation of this file.
1import React, { useState, useEffect, useRef } from 'react';
2import MainLayout from '../components/Layout/MainLayout';
3// import removed: TenantSelector
4import './Settings.css';
5import { apiFetch } from '../lib/api';
6import { isAdmin } from '../utils/auth';
7import { notifySuccess, notifyError } from '../utils/notifications';
8
9function Settings() {
10 // selectedTenantId should come from props/context, not localStorage
11 // Remove localStorage usage for tenant selection
12 // -------------------------------
13 // White Label Branding State
14 // -------------------------------
15 const [iconPreview, setIconPreview] = useState(null);
16 const [iconUploading, setIconUploading] = useState(false);
17 const [iconError, setIconError] = useState(null);
18 const iconInputRef = useRef();
19
20 // -------------------------------
21 // Main Settings State
22 // -------------------------------
23 const [settings, setSettings] = useState(null);
24 const [loading, setLoading] = useState(true);
25 const [error, setError] = useState(null);
26 const [saving, setSaving] = useState(false);
27
28 const [taxRate, setTaxRate] = useState(10);
29 const [taxRateLoading, setTaxRateLoading] = useState(true);
30
31 const [invoiceAdvanceDays, setInvoiceAdvanceDays] = useState(14);
32 const [invoiceAdvanceDaysLoading, setInvoiceAdvanceDaysLoading] = useState(true);
33
34 const [companySettings, setCompanySettings] = useState({
35 abn: '',
36 address: '',
37 phone: '',
38 email: ''
39 });
40 const [companySettingsLoading, setCompanySettingsLoading] = useState(true);
41
42 const [users, setUsers] = useState([]);
43 const [usersLoading, setUsersLoading] = useState(true);
44 const [userError, setUserError] = useState(null);
45 const [searchTerm, setSearchTerm] = useState('');
46
47 const [newUser, setNewUser] = useState({
48 name: '',
49 email: '',
50 role: 'staff',
51 password: '',
52 tenant_id: null
53 });
54 const [creatingUser, setCreatingUser] = useState(false);
55
56 // User editing state
57 const [editingUser, setEditingUser] = useState(null);
58 const [editUserData, setEditUserData] = useState({
59 name: '',
60 email: '',
61 role: ''
62 });
63 const [updatingUser, setUpdatingUser] = useState(false);
64
65 // Create user modal state
66 const [showCreateUserModal, setShowCreateUserModal] = useState(false);
67
68 const [currentRole, setCurrentRole] = useState(null);
69 const [isRootUser, setIsRootUser] = useState(false);
70
71 const [tenants, setTenants] = useState([]);
72 const [tenantSearch, setTenantSearch] = useState('');
73 const [filteredTenants, setFilteredTenants] = useState([]);
74 const [tenantDropdownOpen, setTenantDropdownOpen] = useState(false);
75 const [selectedTenant, setSelectedTenant] = useState(null);
76 const [selectedTenantId, setSelectedTenantId] = useState('');
77
78 // -------------------------------
79 // WordPress Settings State (Root Tenant Only)
80 // -------------------------------
81 const [wordpressSettings, setWordpressSettings] = useState({
82 database_password: '',
83 spaces_access_key: '',
84 spaces_secret_key: ''
85 });
86 const [wordpressLoading, setWordpressLoading] = useState(false);
87 const [wordpressSaving, setWordpressSaving] = useState(false);
88 const [wordpressError, setWordpressError] = useState(null);
89 const [wordpressSuccess, setWordpressSuccess] = useState(false);
90
91 // -------------------------------
92 // Pax8 Integration State
93 // -------------------------------
94 const [pax8Settings, setPax8Settings] = useState({
95 client_id: '',
96 client_secret: '',
97 pax8_company_id: ''
98 });
99 const [pax8Loading, setPax8Loading] = useState(false);
100 const [pax8Saving, setPax8Saving] = useState(false);
101 const [pax8Error, setPax8Error] = useState(null);
102 const [pax8Success, setPax8Success] = useState(false);
103 const [pax8Testing, setPax8Testing] = useState(false);
104 const [pax8TestResult, setPax8TestResult] = useState(null);
105 const [pax8Configured, setPax8Configured] = useState(false);
106
107 // -------------------------------
108 // Load Tenant Icon
109 // -------------------------------
110 useEffect(() => {
111 if (!selectedTenantId) return;
112
113 apiFetch(`/tenants/${selectedTenantId}/icon`)
114 .then(res => res.ok ? res.blob() : null)
115 .then(blob => {
116 if (blob) setIconPreview(URL.createObjectURL(blob));
117 else setIconPreview(null);
118 });
119 }, [selectedTenantId]);
120
121 // -------------------------------
122 // Set Favicon
123 // -------------------------------
124 useEffect(() => {
125 if (!iconPreview) return;
126
127 let link = document.querySelector("link[rel~='icon']");
128 if (!link) {
129 link = document.createElement('link');
130 link.rel = 'icon';
131 document.head.appendChild(link);
132 }
133 link.href = iconPreview;
134
135 return () => {
136 link.href = '/favicon.ico';
137 };
138 }, [iconPreview]);
139
140 // -------------------------------
141 // Icon Upload
142 // -------------------------------
143 const handleIconUpload = async (e) => {
144 setIconUploading(true);
145 setIconError(null);
146
147 const file = e.target.files[0];
148 if (!file) return;
149
150 const formData = new FormData();
151 formData.append('icon', file);
152
153 try {
154 const token = localStorage.getItem('token');
155
156 const res = await apiFetch(`/tenants/${selectedTenantId}/icon`, {
157 method: 'POST',
158 headers: { Authorization: `Bearer ${token}` },
159 body: formData
160 });
161
162 if (!res.ok) throw new Error('Upload failed');
163
164 setIconPreview(URL.createObjectURL(file));
165 } catch (err) {
166 setIconError('Upload failed');
167 } finally {
168 setIconUploading(false);
169 }
170 };
171
172 // -------------------------------
173 // Tenant-aware Headers
174 // -------------------------------
175 const getTenantHeaders = async (base = {}) => {
176 const token = localStorage.getItem('token');
177 if (!token) return base;
178
179 const headers = { ...base };
180
181 try {
182 const payload = JSON.parse(atob(token.split('.')[1]));
183 const isMSP = payload.role === 'admin' || payload.role === 'msp';
184
185 if (isMSP && selectedTenantId) {
186 // No more subdomain header — JWT handles tenancy now
187 }
188 } catch { }
189
190 return headers;
191 };
192
193 // -------------------------------
194 // Fetch Users
195 // -------------------------------
196 const fetchUsers = async (search) => {
197 setUsersLoading(true);
198 try {
199 const token = localStorage.getItem('token');
200 let url = '/users';
201 if (search?.trim()) url += `?search=${encodeURIComponent(search.trim())}`;
202
203 const res = await apiFetch(url, {
204 headers: { Authorization: `Bearer ${token}` }
205 });
206
207 if (!res.ok) throw new Error('Failed to load users');
208
209 setUsers(await res.json());
210 setUserError(null);
211 } catch (err) {
212 setUserError(err.message || 'Error');
213 } finally {
214 setUsersLoading(false);
215 }
216 };
217
218 // -------------------------------
219 // Fetch Settings, Tax, Company Info
220 // -------------------------------
221 useEffect(() => {
222 const fetchAll = async () => {
223 // ---- General Settings ----
224 setLoading(true);
225 try {
226 const token = localStorage.getItem('token');
227 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
228
229 const res = await apiFetch('/settings', { headers });
230 if (!res.ok) throw new Error('Failed to load settings');
231
232 const data = await res.json();
233 setSettings(data);
234 localStorage.setItem('app_settings', JSON.stringify(data));
235 setError(null);
236 } catch (err) {
237 setError(err.message);
238 } finally {
239 setLoading(false);
240 }
241
242 // ---- Tax ----
243 setTaxRateLoading(true);
244 try {
245 const token = localStorage.getItem('token');
246 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
247
248 const res = await apiFetch('/settings/default_tax_rate', { headers });
249 if (res.ok) {
250 const data = await res.json();
251 setTaxRate(parseFloat(data.setting_value) || 10);
252 }
253 } finally {
254 setTaxRateLoading(false);
255 }
256
257 // ---- Invoice Advance Days ----
258 setInvoiceAdvanceDaysLoading(true);
259 try {
260 const token = localStorage.getItem('token');
261 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
262
263 const res = await apiFetch('/settings/invoice_advance_days', { headers });
264 if (res.ok) {
265 const data = await res.json();
266 setInvoiceAdvanceDays(parseInt(data.setting_value, 10) || 14);
267 }
268 } finally {
269 setInvoiceAdvanceDaysLoading(false);
270 }
271
272 // ---- Company Info ----
273 setCompanySettingsLoading(true);
274 try {
275 const token = localStorage.getItem('token');
276 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
277
278 const [abn, address, phone, email] = await Promise.all([
279 apiFetch('/settings/company_abn', { headers }),
280 apiFetch('/settings/company_address', { headers }),
281 apiFetch('/settings/company_phone', { headers }),
282 apiFetch('/settings/company_email', { headers })
283 ]);
284
285 setCompanySettings({
286 abn: abn.ok ? (await abn.json()).setting_value : '',
287 address: address.ok ? (await address.json()).setting_value : '',
288 phone: phone.ok ? (await phone.json()).setting_value : '',
289 email: email.ok ? (await email.json()).setting_value : ''
290 });
291 } finally {
292 setCompanySettingsLoading(false);
293 }
294 };
295
296 fetchAll();
297 }, [selectedTenantId]);
298
299 // -------------------------------
300 // Initial Load: Role, Root, Tenants, Users
301 // -------------------------------
302 useEffect(() => {
303 const token = localStorage.getItem('token');
304
305 // Role
306 if (token) {
307 try {
308 const payload = JSON.parse(atob(token.split('.')[1]));
309 setCurrentRole(payload.role);
310 } catch { }
311 }
312
313 // Root check + Tenants
314 const checkRoot = async () => {
315 if (!token) return;
316
317 try {
318 const res = await apiFetch('/tenants', {
319 headers: { Authorization: `Bearer ${token}` }
320 });
321
322 setIsRootUser(res.ok);
323
324 if (res.ok) {
325 const data = await res.json();
326 setTenants(data.tenants || []);
327 setFilteredTenants(data.tenants || []);
328 }
329 } catch {
330 setIsRootUser(false);
331 }
332 };
333
334 checkRoot();
335 fetchUsers();
336 }, []);
337
338 // -------------------------------
339 // Fetch WordPress Settings (Root Tenant Only)
340 // -------------------------------
341 const fetchWordPressSettings = async () => {
342 if (!isRootUser) return; // Only fetch if root user
343
344 setWordpressLoading(true);
345 setWordpressError(null);
346 try {
347 const token = localStorage.getItem('token');
348 const res = await apiFetch('/settings/wordpress', {
349 headers: { Authorization: `Bearer ${token}` }
350 });
351
352 if (res.ok) {
353 const data = await res.json();
354 setWordpressSettings(data);
355 } else if (res.status === 403) {
356 // Not root user, ignore
357 console.log('Not root user, skipping WordPress settings');
358 } else {
359 throw new Error('Failed to load WordPress settings');
360 }
361 } catch (err) {
362 console.error('Error fetching WordPress settings:', err);
363 setWordpressError(err.message || 'Error loading WordPress settings');
364 } finally {
365 setWordpressLoading(false);
366 }
367 };
368
369 // -------------------------------
370 // Save WordPress Settings (Root Tenant Only)
371 // -------------------------------
372 const saveWordPressSettings = async () => {
373 if (!wordpressSettings.database_password || !wordpressSettings.spaces_access_key || !wordpressSettings.spaces_secret_key) {
374 setWordpressError('All fields are required');
375 return;
376 }
377
378 setWordpressSaving(true);
379 setWordpressError(null);
380 setWordpressSuccess(false);
381 try {
382 const token = localStorage.getItem('token');
383 const res = await apiFetch('/settings/wordpress', {
384 method: 'POST',
385 headers: {
386 'Content-Type': 'application/json',
387 Authorization: `Bearer ${token}`
388 },
389 body: JSON.stringify(wordpressSettings)
390 });
391
392 if (!res.ok) {
393 const errorData = await res.json();
394 throw new Error(errorData.error || 'Failed to save WordPress settings');
395 }
396
397 setWordpressSuccess(true);
398 setTimeout(() => setWordpressSuccess(false), 3000);
399 } catch (err) {
400 console.error('Error saving WordPress settings:', err);
401 setWordpressError(err.message || 'Error saving WordPress settings');
402 } finally {
403 setWordpressSaving(false);
404 }
405 };
406
407 // Debounce search
408 useEffect(() => {
409 const t = setTimeout(() => fetchUsers(searchTerm), 300);
410 return () => clearTimeout(t);
411 }, [searchTerm]);
412
413 // Tenant search filter
414 useEffect(() => {
415 if (!tenantSearch.trim()) {
416 setFilteredTenants(tenants);
417 } else {
418 const s = tenantSearch.toLowerCase();
419 setFilteredTenants(
420 tenants.filter(t => t.name.toLowerCase().includes(s))
421 );
422 }
423 }, [tenantSearch, tenants]);
424
425 // Fetch WordPress settings when root user status is confirmed
426 useEffect(() => {
427 if (isRootUser) {
428 fetchWordPressSettings();
429 }
430 }, [isRootUser]);
431
432 // -------------------------------
433 // Pax8 Integration Functions
434 // -------------------------------
435 const fetchPax8Config = async () => {
436 setPax8Loading(true);
437 setPax8Error(null);
438 try {
439 const token = localStorage.getItem('token');
440 const res = await apiFetch('/office365/config', {
441 headers: { Authorization: `Bearer ${token}` }
442 });
443
444 if (res.ok) {
445 const data = await res.json();
446 if (data.configured) {
447 setPax8Configured(true);
448 setPax8Settings({
449 client_id: data.config.client_id || '',
450 client_secret: '', // Never send secret back
451 pax8_company_id: data.config.pax8_company_id || ''
452 });
453 } else {
454 setPax8Configured(false);
455 }
456 }
457 } catch (err) {
458 console.error('Error fetching Pax8 config:', err);
459 setPax8Error(err.message || 'Error loading Pax8 configuration');
460 } finally {
461 setPax8Loading(false);
462 }
463 };
464
465 const savePax8Config = async () => {
466 if (!pax8Settings.client_id || !pax8Settings.client_secret) {
467 setPax8Error('Client ID and Client Secret are required');
468 return;
469 }
470
471 setPax8Saving(true);
472 setPax8Error(null);
473 setPax8Success(false);
474 try {
475 const token = localStorage.getItem('token');
476 const res = await apiFetch('/office365/config', {
477 method: 'PUT',
478 headers: {
479 'Content-Type': 'application/json',
480 Authorization: `Bearer ${token}`
481 },
482 body: JSON.stringify(pax8Settings)
483 });
484
485 if (!res.ok) {
486 const errorData = await res.json();
487 throw new Error(errorData.error || 'Failed to save Pax8 configuration');
488 }
489
490 setPax8Success(true);
491 setPax8Configured(true);
492 setTimeout(() => setPax8Success(false), 3000);
493 } catch (err) {
494 console.error('Error saving Pax8 config:', err);
495 setPax8Error(err.message || 'Error saving Pax8 configuration');
496 } finally {
497 setPax8Saving(false);
498 }
499 };
500
501 const testPax8Connection = async () => {
502 setPax8Testing(true);
503 setPax8TestResult(null);
504 setPax8Error(null);
505 try {
506 const token = localStorage.getItem('token');
507 const res = await apiFetch('/office365/config/test', {
508 method: 'POST',
509 headers: { Authorization: `Bearer ${token}` }
510 });
511
512 const data = await res.json();
513 if (res.ok && data.success) {
514 setPax8TestResult({ success: true, message: 'Connection successful! Pax8 API is responding.' });
515 } else {
516 setPax8TestResult({ success: false, message: data.error || 'Connection test failed' });
517 }
518 } catch (err) {
519 console.error('Error testing Pax8 connection:', err);
520 setPax8TestResult({ success: false, message: err.message || 'Connection test failed' });
521 } finally {
522 setPax8Testing(false);
523 }
524 };
525
526 // Fetch Pax8 config on load
527 useEffect(() => {
528 fetchPax8Config();
529 }, [selectedTenantId]);
530
531 // -------------------------------
532 // Update User Function
533 // -------------------------------
534 const updateUser = async () => {
535 if (!editingUser || !editUserData.name || !editUserData.email) {
536 alert('Please fill in all required fields');
537 return;
538 }
539
540 setUpdatingUser(true);
541 try {
542 const token = localStorage.getItem('token');
543 const res = await apiFetch(`/users/${editingUser.user_id}`, {
544 method: 'PUT',
545 headers: {
546 'Content-Type': 'application/json',
547 Authorization: `Bearer ${token}`
548 },
549 body: JSON.stringify({
550 name: editUserData.name,
551 email: editUserData.email,
552 role: editUserData.role
553 })
554 });
555
556 if (!res.ok) {
557 const errorText = await res.text();
558 throw new Error(errorText || 'Update failed');
559 }
560
561 // Refresh users list
562 await fetchUsers();
563
564 // Close edit modal
565 setEditingUser(null);
566 setEditUserData({ name: '', email: '', role: '' });
567
568 alert('User updated successfully');
569 } catch (err) {
570 alert('Failed to update user: ' + (err.message || err));
571 } finally {
572 setUpdatingUser(false);
573 }
574 };
575
576 const startEditUser = (user) => {
577 setEditingUser(user);
578 setEditUserData({
579 name: user.name,
580 email: user.email,
581 role: user.role
582 });
583 };
584
585 const cancelEditUser = () => {
586 setEditingUser(null);
587 setEditUserData({ name: '', email: '', role: '' });
588 };
589
590 const openCreateUserModal = () => {
591 setShowCreateUserModal(true);
592
593 // Pre-populate tenant if one is currently selected
594 if (selectedTenantId && tenants.length > 0) {
595 const currentTenant = tenants.find(t => t.tenant_id === selectedTenantId);
596 if (currentTenant) {
597 setSelectedTenant(currentTenant);
598 setNewUser({
599 name: '',
600 email: '',
601 role: 'staff',
602 password: '',
603 tenant_id: currentTenant.tenant_id
604 });
605 setTenantSearch('');
606 return;
607 }
608 }
609
610 // Default: no tenant pre-selected
611 setNewUser({
612 name: '',
613 email: '',
614 role: 'staff',
615 password: '',
616 tenant_id: null
617 });
618 setTenantSearch('');
619 setSelectedTenant(null);
620 };
621
622 const closeCreateUserModal = () => {
623 setShowCreateUserModal(false);
624 setNewUser({
625 name: '',
626 email: '',
627 role: 'staff',
628 password: '',
629 tenant_id: null
630 });
631 setTenantSearch('');
632 setSelectedTenant(null);
633 };
634
635 // -------------------------------
636 // Render
637 // -------------------------------
638 return (
639 <MainLayout>
640 <div className="settings-page">
641
642 <h1>Settings</h1>
643
644 {!isAdmin() && (
645 <div style={{
646 padding: '16px',
647 backgroundColor: 'var(--warning-bg, #fff3cd)',
648 border: '1px solid var(--warning, #ffc107)',
649 borderRadius: '8px',
650 marginBottom: '20px',
651 color: 'var(--warning-text, #856404)'
652 }}>
653 <strong>⚠️ Read-Only Access:</strong> Staff users can view settings but cannot make changes. Only admin users can modify settings.
654 </div>
655 )}
656
657 {loading && <div>Loading...</div>}
658 {error && <div className="error">Error: {error}</div>}
659
660 {settings && (
661 <div className="settings-sections" style={{
662 opacity: !isAdmin() ? 0.7 : 1,
663 pointerEvents: !isAdmin() ? 'none' : 'auto',
664 userSelect: !isAdmin() ? 'none' : 'auto'
665 }}>
666
667 {/* ------------------------------------------------ */}
668 {/* 1. EMAIL AUTOMATION */}
669 {/* ------------------------------------------------ */}
670 <section className="settings-section">
671 <h2>Email Automation</h2>
672
673 <div className="settings-form">
674
675 <div className="form-group">
676 <label>Email Server Type</label>
677 <select
678 value={settings.emailAutomation?.serverType ?? 'imap'}
679 onChange={e => setSettings(s => ({
680 ...s,
681 emailAutomation: {
682 ...s.emailAutomation,
683 serverType: e.target.value
684 }
685 }))}
686 >
687 <option value="imap">IMAP</option>
688 <option value="smtp">SMTP</option>
689 </select>
690 </div>
691
692 <div className="form-group">
693 <label>Server Host</label>
694 <input
695 type="text"
696 value={settings.emailAutomation?.host ?? ''}
697 onChange={e => setSettings(s => ({
698 ...s,
699 emailAutomation: {
700 ...s.emailAutomation,
701 host: e.target.value
702 }
703 }))}
704 placeholder="mail.example.com"
705 />
706 </div>
707
708 <div className="form-group">
709 <label>Server Port</label>
710 <input
711 type="number"
712 value={settings.emailAutomation?.port ?? 993}
713 onChange={e => setSettings(s => ({
714 ...s,
715 emailAutomation: {
716 ...s.emailAutomation,
717 port: parseInt(e.target.value || '993', 10)
718 }
719 }))}
720 placeholder="993"
721 />
722 </div>
723
724 <div className="form-group">
725 <label>Mailbox (e.g. INBOX)</label>
726 <input
727 type="text"
728 value={settings.emailAutomation?.mailbox ?? 'INBOX'}
729 onChange={e => setSettings(s => ({
730 ...s,
731 emailAutomation: {
732 ...s.emailAutomation,
733 mailbox: e.target.value
734 }
735 }))}
736 placeholder="INBOX"
737 />
738 </div>
739
740 <div className="form-group">
741 <label>Email Username</label>
742 <input
743 type="text"
744 value={settings.emailAutomation?.username ?? ''}
745 onChange={e => setSettings(s => ({
746 ...s,
747 emailAutomation: {
748 ...s.emailAutomation,
749 username: e.target.value
750 }
751 }))}
752 placeholder="user@example.com"
753 />
754 </div>
755
756 <div className="form-group">
757 <label>Email Password</label>
758 <input
759 type="password"
760 value={settings.emailAutomation?.password ?? ''}
761 onChange={e => setSettings(s => ({
762 ...s,
763 emailAutomation: {
764 ...s.emailAutomation,
765 password: e.target.value
766 }
767 }))}
768 placeholder="••••••••"
769 />
770 </div>
771
772 <div className="form-group">
773 <label>Connection Security</label>
774 <select
775 value={settings.emailAutomation?.security ?? 'ssl'}
776 onChange={e => setSettings(s => ({
777 ...s,
778 emailAutomation: {
779 ...s.emailAutomation,
780 security: e.target.value
781 }
782 }))}
783 >
784 <option value="ssl">SSL/TLS</option>
785 <option value="starttls">STARTTLS</option>
786 <option value="none">None</option>
787 </select>
788 </div>
789
790 <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
791 Configure email server connection for ticket automation. Credentials are stored securely per tenant.
792 </div>
793 </div>
794 </section>
795
796 {/* ------------------------------------------------ */}
797 {/* 2. COMPANY INFORMATION */}
798 {/* ------------------------------------------------ */}
799 <section className="settings-section">
800 <h2>Company Information (for Invoices)</h2>
801
802 <div className="settings-form">
803
804 <div className="form-group">
805 <label>Company ABN</label>
806 <input
807 id="company-abn"
808 name="company-abn"
809 type="text"
810 value={companySettings.abn}
811 onChange={(e) => setCompanySettings(s => ({
812 ...s,
813 abn: e.target.value
814 }))}
815 disabled={companySettingsLoading}
816 placeholder="12 345 678 901"
817 />
818 <small style={{ display: 'block', marginTop: '4px', color: '#666' }}>
819 Your Australian Business Number (appears on invoices)
820 </small>
821 </div>
822
823 <div className="form-group">
824 <label>Business Address</label>
825 <input
826 id="company-address"
827 name="company-address"
828 type="text"
829 value={companySettings.address}
830 onChange={(e) => setCompanySettings(s => ({
831 ...s,
832 address: e.target.value
833 }))}
834 disabled={companySettingsLoading}
835 placeholder="123 Business St, City, State, Postcode"
836 />
837 </div>
838
839 <div className="form-group">
840 <label>Business Phone</label>
841 <input
842 id="company-phone"
843 name="company-phone"
844 type="text"
845 value={companySettings.phone}
846 onChange={(e) => setCompanySettings(s => ({
847 ...s,
848 phone: e.target.value
849 }))}
850 disabled={companySettingsLoading}
851 placeholder="(02) 1234 5678"
852 />
853 </div>
854
855 <div className="form-group">
856 <label>Business Email</label>
857 <input
858 id="company-email"
859 name="company-email"
860 type="email"
861 value={companySettings.email}
862 onChange={(e) => setCompanySettings(s => ({
863 ...s,
864 email: e.target.value
865 }))}
866 disabled={companySettingsLoading}
867 placeholder="info@yourcompany.com.au"
868 />
869 </div>
870
871 <button
872 className="btn primary"
873 onClick={async () => {
874 try {
875 const token = localStorage.getItem('token');
876 const headers = await getTenantHeaders({
877 'Content-Type': 'application/json',
878 Authorization: `Bearer ${token}`
879 });
880
881 await Promise.all([
882 apiFetch('/settings/company_abn', {
883 method: 'PUT',
884 headers,
885 body: JSON.stringify({
886 setting_value: companySettings.abn,
887 description: 'Company ABN'
888 })
889 }),
890
891 apiFetch('/settings/company_address', {
892 method: 'PUT',
893 headers,
894 body: JSON.stringify({
895 setting_value: companySettings.address,
896 description: 'Company Address'
897 })
898 }),
899
900 apiFetch('/settings/company_phone', {
901 method: 'PUT',
902 headers,
903 body: JSON.stringify({
904 setting_value: companySettings.phone,
905 description: 'Company Phone'
906 })
907 }),
908
909 apiFetch('/settings/company_email', {
910 method: 'PUT',
911 headers,
912 body: JSON.stringify({
913 setting_value: companySettings.email,
914 description: 'Company Email'
915 })
916 })
917 ]);
918
919 alert('Company information updated successfully');
920
921 } catch (err) {
922 console.error('Failed to update company information:', err);
923 alert('Failed to update company information: ' + err.message);
924 }
925 }}
926 >
927 Save Company Information
928 </button>
929
930 </div>
931 </section>
932
933 {/* ------------------------------------------------ */}
934 {/* 3. TAX SETTINGS */}
935 {/* ------------------------------------------------ */}
936 {/* 3. BILLING & INVOICING SETTINGS */}
937 {/* ------------------------------------------------ */}
938 <section className="settings-section">
939 <h2>Billing & Invoicing</h2>
940
941 <div className="settings-form">
942
943 <div className="form-group">
944 <label>Default Tax Rate (GST %)</label>
945 <input
946 id="company-tax-rate"
947 name="company-tax-rate"
948 type="number"
949 step="0.01"
950 value={taxRate}
951 onChange={(e) =>
952 setTaxRate(parseFloat(e.target.value) || 0)
953 }
954 disabled={taxRateLoading}
955 />
956 <small style={{
957 display: 'block',
958 marginTop: '4px',
959 color: '#666'
960 }}>
961 Australia GST is 10%. This rate will be applied to all new invoices.
962 </small>
963 </div>
964
965 <div className="form-group">
966 <label>Invoice Advance Days</label>
967 <input
968 id="invoice-advance-days"
969 name="invoice-advance-days"
970 type="number"
971 min="0"
972 max="90"
973 step="1"
974 value={invoiceAdvanceDays}
975 onChange={(e) =>
976 setInvoiceAdvanceDays(parseInt(e.target.value, 10) || 0)
977 }
978 disabled={invoiceAdvanceDaysLoading}
979 />
980 <small style={{
981 display: 'block',
982 marginTop: '4px',
983 color: '#666'
984 }}>
985 Generate invoices this many days before renewal date. Default is 14 days. Applies to all contracts with auto-invoicing enabled.
986 </small>
987 </div>
988
989 <button
990 className="btn primary"
991 onClick={async () => {
992 try {
993 const token = localStorage.getItem('token');
994 const headers = await getTenantHeaders({
995 'Content-Type': 'application/json',
996 Authorization: `Bearer ${token}`
997 });
998
999 await Promise.all([
1000 apiFetch('/settings/default_tax_rate', {
1001 method: 'PUT',
1002 headers,
1003 body: JSON.stringify({
1004 setting_value: taxRate.toString(),
1005 description: 'Default GST/Tax rate percentage'
1006 })
1007 }),
1008 apiFetch('/settings/invoice_advance_days', {
1009 method: 'PUT',
1010 headers,
1011 body: JSON.stringify({
1012 setting_value: invoiceAdvanceDays.toString(),
1013 description: 'Days before renewal to generate invoice'
1014 })
1015 })
1016 ]);
1017
1018 alert('Billing settings updated successfully');
1019
1020 } catch (err) {
1021 alert('Failed to update billing settings: ' + err.message);
1022 }
1023 }}
1024 >
1025 Save Billing Settings
1026 </button>
1027
1028 </div>
1029 </section>
1030
1031 {/* ------------------------------------------------ */}
1032 {/* 4. GENERAL SETTINGS */}
1033 {/* ------------------------------------------------ */}
1034 <section className="settings-section">
1035 <h2>General Settings</h2>
1036
1037 <div className="settings-form">
1038
1039 <div className="form-group">
1040 <label>Company Name</label>
1041 <input
1042 id="company-name"
1043 name="company-name"
1044 type="text"
1045 value={settings.general.companyName}
1046 onChange={(e) =>
1047 setSettings(s => ({
1048 ...s,
1049 general: {
1050 ...s.general,
1051 companyName: e.target.value
1052 }
1053 }))
1054 }
1055 />
1056 </div>
1057
1058 <div className="form-group">
1059 <label>Timezone</label>
1060 <input
1061 id="company-timezone"
1062 name="company-timezone"
1063 type="text"
1064 value={settings.general.timezone}
1065 onChange={(e) =>
1066 setSettings(s => ({
1067 ...s,
1068 general: {
1069 ...s.general,
1070 timezone: e.target.value
1071 }
1072 }))
1073 }
1074 />
1075 </div>
1076
1077 <div className="form-group">
1078 <label>Date Format</label>
1079 <input
1080 id="company-date-format"
1081 name="company-date-format"
1082 type="text"
1083 value={settings.general.dateFormat}
1084 onChange={(e) =>
1085 setSettings(s => ({
1086 ...s,
1087 general: {
1088 ...s.general,
1089 dateFormat: e.target.value
1090 }
1091 }))
1092 }
1093 />
1094 </div>
1095
1096 <button
1097 className="btn primary"
1098 onClick={async () => {
1099 setSaving(true);
1100 try {
1101 const token = localStorage.getItem('token');
1102 const headers = await getTenantHeaders({
1103 'Content-Type': 'application/json',
1104 Authorization: `Bearer ${token}`
1105 });
1106
1107 const res = await apiFetch('/settings', {
1108 method: 'PUT',
1109 headers,
1110 body: JSON.stringify(settings)
1111 });
1112
1113 if (!res.ok) {
1114 const txt = await res.text();
1115 throw new Error(txt || 'Failed to save settings');
1116 }
1117
1118 const saved = await res.json();
1119 setSettings(saved);
1120 try {
1121 localStorage.setItem('app_settings', JSON.stringify(saved));
1122 } catch { }
1123
1124 try {
1125 applyThemeFromSettings(saved);
1126 } catch { }
1127
1128 window.dispatchEvent(
1129 new CustomEvent('settingsUpdated', { detail: saved })
1130 );
1131
1132 await notifySuccess('Settings Saved', 'General settings have been saved successfully');
1133
1134 } catch (err) {
1135 await notifyError('Save Failed', err.message || 'Failed to save settings');
1136 } finally {
1137 setSaving(false);
1138 }
1139 }}
1140 disabled={saving}
1141 >
1142 {saving ? 'Saving...' : 'Save General Settings'}
1143 </button>
1144
1145 </div>
1146 </section>
1147
1148 {/* ------------------------------------------------ */}
1149 {/* 5. WHITE LABEL BRANDING */}
1150 {/* ------------------------------------------------ */}
1151 <section className="settings-section">
1152 <h2>White Label Branding</h2>
1153
1154 <div className="settings-form">
1155
1156 <div className="form-group">
1157 <label>Company Icon (for installer & dashboard)</label>
1158
1159 <input
1160 type="file"
1161 accept="image/png,image/x-icon"
1162 ref={iconInputRef}
1163 onChange={handleIconUpload}
1164 disabled={iconUploading}
1165 />
1166
1167 {iconPreview && (
1168 <div style={{ marginTop: 12 }}>
1169 <img
1170 src={iconPreview}
1171 alt="Company Icon"
1172 style={{
1173 width: 64,
1174 height: 64,
1175 borderRadius: 8,
1176 boxShadow: '0 0 4px #ccc'
1177 }}
1178 />
1179 <div
1180 style={{
1181 fontSize: 12,
1182 color: '#888',
1183 marginTop: 4
1184 }}
1185 >
1186 Preview
1187 </div>
1188 </div>
1189 )}
1190
1191 {iconUploading && <div>Uploading...</div>}
1192 {iconError && <div className="error">{iconError}</div>}
1193
1194 <div
1195 style={{
1196 fontSize: 12,
1197 color: '#888',
1198 marginTop: 8
1199 }}
1200 >
1201 This icon will be used for your installer and dashboard browser tab.
1202 </div>
1203 </div>
1204
1205 </div>
1206 </section>
1207
1208 {/* ------------------------------------------------ */}
1209 {/* 6. WORDPRESS SETTINGS (Root Tenant Only) */}
1210 {/* ------------------------------------------------ */}
1211 {isRootUser && (
1212 <section className="settings-section">
1213 <h2>WordPress Settings (Global)</h2>
1214 <div style={{ fontSize: '14px', color: '#666', marginBottom: '16px' }}>
1215 These credentials will be used automatically when creating new WordPress sites.
1216 Only visible to root tenant administrators.
1217 </div>
1218
1219 <div className="settings-form">
1220 <div className="form-group">
1221 <label htmlFor="wp-db-password">Database Password *</label>
1222 <input
1223 id="wp-db-password"
1224 type="password"
1225 value={wordpressSettings.database_password}
1226 onChange={(e) => setWordpressSettings(prev => ({
1227 ...prev,
1228 database_password: e.target.value
1229 }))}
1230 placeholder="Strong database password"
1231 disabled={wordpressLoading}
1232 />
1233 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1234 MySQL database password for all WordPress sites
1235 </div>
1236 </div>
1237
1238 <div className="form-group">
1239 <label htmlFor="wp-spaces-key">DO Spaces Access Key *</label>
1240 <input
1241 id="wp-spaces-key"
1242 type="text"
1243 value={wordpressSettings.spaces_access_key}
1244 onChange={(e) => setWordpressSettings(prev => ({
1245 ...prev,
1246 spaces_access_key: e.target.value
1247 }))}
1248 placeholder="DO00..."
1249 disabled={wordpressLoading}
1250 />
1251 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1252 DigitalOcean Spaces access key for media storage
1253 </div>
1254 </div>
1255
1256 <div className="form-group">
1257 <label htmlFor="wp-spaces-secret">DO Spaces Secret Key *</label>
1258 <input
1259 id="wp-spaces-secret"
1260 type="password"
1261 value={wordpressSettings.spaces_secret_key}
1262 onChange={(e) => setWordpressSettings(prev => ({
1263 ...prev,
1264 spaces_secret_key: e.target.value
1265 }))}
1266 placeholder="Secret key"
1267 disabled={wordpressLoading}
1268 />
1269 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1270 DigitalOcean Spaces secret key for media storage
1271 </div>
1272 </div>
1273
1274 {wordpressError && (
1275 <div style={{ padding: '12px', background: '#fee', border: '1px solid #fcc', borderRadius: '4px', color: '#c33', marginBottom: '12px' }}>
1276 {wordpressError}
1277 </div>
1278 )}
1279
1280 {wordpressSuccess && (
1281 <div style={{ padding: '12px', background: '#efe', border: '1px solid #cfc', borderRadius: '4px', color: '#3c3', marginBottom: '12px' }}>
1282 WordPress settings saved successfully!
1283 </div>
1284 )}
1285
1286 <button
1287 className="btn primary"
1288 onClick={saveWordPressSettings}
1289 disabled={wordpressSaving || wordpressLoading}
1290 >
1291 {wordpressSaving ? 'Saving...' : 'Save WordPress Settings'}
1292 </button>
1293 </div>
1294 </section>
1295 )}
1296
1297 {/* ------------------------------------------------ */}
1298 {/* 7. PAX8 MICROSOFT 365 INTEGRATION */}
1299 {/* ------------------------------------------------ */}
1300 <section className="settings-section">
1301 <h2>Pax8 Microsoft 365 Integration</h2>
1302
1303 <div style={{ fontSize: '14px', color: '#666', marginBottom: '16px' }}>
1304 Configure Pax8 API credentials to enable Microsoft 365 license purchasing and management.
1305 {pax8Configured && (
1306 <span style={{ color: '#3c3', marginLeft: '8px', fontWeight: 'bold' }}>
1307 ✓ Configured
1308 </span>
1309 )}
1310 </div>
1311
1312 <div className="settings-form">
1313 <div className="form-group">
1314 <label htmlFor="pax8-client-id">Pax8 Client ID *</label>
1315 <input
1316 id="pax8-client-id"
1317 type="text"
1318 value={pax8Settings.client_id}
1319 onChange={(e) => setPax8Settings(prev => ({
1320 ...prev,
1321 client_id: e.target.value
1322 }))}
1323 placeholder="Enter Pax8 OAuth client ID"
1324 disabled={pax8Loading}
1325 />
1326 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1327 OAuth 2.0 client ID from Pax8 partner portal
1328 </div>
1329 </div>
1330
1331 <div className="form-group">
1332 <label htmlFor="pax8-client-secret">Pax8 Client Secret *</label>
1333 <input
1334 id="pax8-client-secret"
1335 type="password"
1336 value={pax8Settings.client_secret}
1337 onChange={(e) => setPax8Settings(prev => ({
1338 ...prev,
1339 client_secret: e.target.value
1340 }))}
1341 placeholder="Enter Pax8 OAuth client secret"
1342 disabled={pax8Loading}
1343 />
1344 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1345 OAuth 2.0 client secret (will be encrypted when saved)
1346 </div>
1347 </div>
1348
1349 <div className="form-group">
1350 <label htmlFor="pax8-company-id">Pax8 Company ID (Optional)</label>
1351 <input
1352 id="pax8-company-id"
1353 type="text"
1354 value={pax8Settings.pax8_company_id}
1355 onChange={(e) => setPax8Settings(prev => ({
1356 ...prev,
1357 pax8_company_id: e.target.value
1358 }))}
1359 placeholder="Your Pax8 company ID"
1360 disabled={pax8Loading}
1361 />
1362 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1363 Your partner company ID in Pax8 (optional)
1364 </div>
1365 </div>
1366
1367 {pax8Error && (
1368 <div style={{ padding: '12px', background: '#fee', border: '1px solid #fcc', borderRadius: '4px', color: '#c33', marginBottom: '12px' }}>
1369 {pax8Error}
1370 </div>
1371 )}
1372
1373 {pax8Success && (
1374 <div style={{ padding: '12px', background: '#efe', border: '1px solid #cfc', borderRadius: '4px', color: '#3c3', marginBottom: '12px' }}>
1375 Pax8 configuration saved successfully!
1376 </div>
1377 )}
1378
1379 {pax8TestResult && (
1380 <div style={{
1381 padding: '12px',
1382 background: pax8TestResult.success ? '#efe' : '#fee',
1383 border: pax8TestResult.success ? '1px solid #cfc' : '1px solid #fcc',
1384 borderRadius: '4px',
1385 color: pax8TestResult.success ? '#3c3' : '#c33',
1386 marginBottom: '12px'
1387 }}>
1388 {pax8TestResult.message}
1389 </div>
1390 )}
1391
1392 <div style={{ display: 'flex', gap: '12px' }}>
1393 <button
1394 className="btn primary"
1395 onClick={savePax8Config}
1396 disabled={pax8Saving || pax8Loading}
1397 >
1398 {pax8Saving ? 'Saving...' : 'Save Configuration'}
1399 </button>
1400
1401 <button
1402 className="btn"
1403 onClick={testPax8Connection}
1404 disabled={pax8Testing || !pax8Configured}
1405 title={!pax8Configured ? 'Save configuration first' : 'Test connection to Pax8 API'}
1406 >
1407 {pax8Testing ? 'Testing...' : 'Test Connection'}
1408 </button>
1409 </div>
1410 </div>
1411 </section>
1412
1413 {/* ------------------------------------------------ */}
1414 {/* 8. NOTIFICATION SETTINGS */}
1415 {/* ------------------------------------------------ */}
1416 <section className="settings-section">
1417 <h2>Notification Settings</h2>
1418
1419 <div className="settings-form">
1420
1421 <div className="form-group">
1422 <label>Email Notifications</label>
1423 <input
1424 id="notifications-email"
1425 name="notifications-email"
1426 type="checkbox"
1427 checked={settings.notifications.emailEnabled}
1428 onChange={(e) =>
1429 setSettings(s => ({
1430 ...s,
1431 notifications: {
1432 ...s.notifications,
1433 emailEnabled: e.target.checked
1434 }
1435 }))
1436 }
1437 />
1438 </div>
1439
1440 <div className="form-group">
1441 <label>Slack Notifications</label>
1442 <input
1443 id="notifications-slack"
1444 name="notifications-slack"
1445 type="checkbox"
1446 checked={settings.notifications.slackEnabled}
1447 onChange={(e) =>
1448 setSettings(s => ({
1449 ...s,
1450 notifications: {
1451 ...s.notifications,
1452 slackEnabled: e.target.checked
1453 }
1454 }))
1455 }
1456 />
1457 </div>
1458 </div>
1459
1460 <h3>Alert Thresholds</h3>
1461
1462 <div className="settings-form">
1463
1464 <div className="form-group">
1465 <label>CPU Warning Level (%)</label>
1466 <input
1467 id="alert-cpu"
1468 name="alert-cpu"
1469 type="number"
1470 value={settings.notifications.alertThresholds.cpu}
1471 onChange={(e) =>
1472 setSettings(s => ({
1473 ...s,
1474 notifications: {
1475 ...s.notifications,
1476 alertThresholds: {
1477 ...s.notifications.alertThresholds,
1478 cpu: parseInt(e.target.value || 0, 10)
1479 }
1480 }
1481 }))
1482 }
1483 />
1484 </div>
1485
1486 <div className="form-group">
1487 <label>Memory Warning Level (%)</label>
1488 <input
1489 id="alert-memory"
1490 name="alert-memory"
1491 type="number"
1492 value={settings.notifications.alertThresholds.memory}
1493 onChange={(e) =>
1494 setSettings(s => ({
1495 ...s,
1496 notifications: {
1497 ...s.notifications,
1498 alertThresholds: {
1499 ...s.notifications.alertThresholds,
1500 memory: parseInt(e.target.value || 0, 10)
1501 }
1502 }
1503 }))
1504 }
1505 />
1506 </div>
1507
1508 <div className="form-group">
1509 <label>Disk Warning Level (%)</label>
1510 <input
1511 id="alert-disk"
1512 name="alert-disk"
1513 type="number"
1514 value={settings.notifications.alertThresholds.disk}
1515 onChange={(e) =>
1516 setSettings(s => ({
1517 ...s,
1518 notifications: {
1519 ...s.notifications,
1520 alertThresholds: {
1521 ...s.notifications.alertThresholds,
1522 disk: parseInt(e.target.value || 0, 10)
1523 }
1524 }
1525 }))
1526 }
1527 />
1528 </div>
1529
1530 </div>
1531 </section>
1532
1533 {/* ------------------------------------------------ */}
1534 {/* 7. DISPLAY SETTINGS */}
1535 {/* ------------------------------------------------ */}
1536 <section className="settings-section">
1537 <h2>Display Settings</h2>
1538
1539 <div className="settings-form">
1540
1541 <div className="form-group">
1542 <label>Items per Page</label>
1543 <input
1544 id="display-items-per-page"
1545 name="display-items-per-page"
1546 type="number"
1547 value={settings.display.itemsPerPage}
1548 onChange={(e) =>
1549 setSettings(s => ({
1550 ...s,
1551 display: {
1552 ...s.display,
1553 itemsPerPage: parseInt(e.target.value || 10, 10)
1554 }
1555 }))
1556 }
1557 />
1558 </div>
1559
1560 <div className="form-group">
1561 <label>Theme</label>
1562 <select
1563 value={settings.display.theme}
1564 onChange={(e) =>
1565 setSettings(s => ({
1566 ...s,
1567 display: {
1568 ...s.display,
1569 theme: e.target.value
1570 }
1571 }))
1572 }
1573 >
1574 <option value="default">Current (Default)</option>
1575 <option value="light">Light</option>
1576 <option value="dark">Dark</option>
1577 <option value="blue">Blue</option>
1578 </select>
1579 </div>
1580
1581 <div className="form-group">
1582 <label>Auto switch theme</label>
1583 <input
1584 id="display-auto-theme"
1585 name="display-auto-theme"
1586 type="checkbox"
1587 checked={settings.display.autoTheme}
1588 onChange={(e) =>
1589 setSettings(s => ({
1590 ...s,
1591 display: {
1592 ...s.display,
1593 autoTheme: e.target.checked
1594 }
1595 }))
1596 }
1597 />
1598 </div>
1599
1600 </div>
1601
1602 <div style={{ marginTop: 12 }}>
1603 <button
1604 className="btn"
1605 onClick={async () => {
1606 setSaving(true);
1607 try {
1608 const token = localStorage.getItem('token');
1609 const headers = await getTenantHeaders({
1610 'Content-Type': 'application/json',
1611 Authorization: `Bearer ${token}`
1612 });
1613
1614 const res = await apiFetch('/settings', {
1615 method: 'PUT',
1616 headers,
1617 body: JSON.stringify(settings)
1618 });
1619
1620 if (!res.ok) {
1621 const txt = await res.text();
1622 throw new Error(txt || 'Failed to save settings');
1623 }
1624
1625 const saved = await res.json();
1626 setSettings(saved);
1627
1628 try {
1629 localStorage.setItem('app_settings', JSON.stringify(saved));
1630 } catch { }
1631
1632 try {
1633 applyThemeFromSettings(saved);
1634 } catch { }
1635
1636 window.dispatchEvent(
1637 new CustomEvent('settingsUpdated', { detail: saved })
1638 );
1639
1640 await notifySuccess('Settings Saved', 'All settings have been saved successfully');
1641
1642 } catch (err) {
1643 await notifyError('Save Failed', err.message || 'Failed to save settings');
1644 } finally {
1645 setSaving(false);
1646 }
1647 }}
1648 >
1649 {saving ? 'Saving...' : 'Save Settings'}
1650 </button>
1651 </div>
1652
1653 </section>
1654
1655
1656 {/* ------------------------------------------------ */}
1657 {/* 8. AUTOMATION & ALERTS */}
1658 {/* (This appears last in your requested order) */}
1659 {/* ------------------------------------------------ */}
1660 <section className="settings-section">
1661 <h2>Automation & Alerts</h2>
1662
1663 <div className="settings-form">
1664
1665 <div className="form-group">
1666 <label>Enable Automation</label>
1667 <input
1668 id="automation-enabled"
1669 name="automation-enabled"
1670 type="checkbox"
1671 checked={settings.automation?.enabled ?? true}
1672 onChange={(e) =>
1673 setSettings(s => ({
1674 ...s,
1675 automation: {
1676 ...s.automation,
1677 enabled: e.target.checked
1678 }
1679 }))
1680 }
1681 />
1682 </div>
1683
1684 <div className="form-group">
1685 <label>Job Queue Priority</label>
1686 <select
1687 id="automation-queue-priority"
1688 name="automation-queue-priority"
1689 value={settings.automation?.queuePriority ?? 'normal'}
1690 onChange={(e) =>
1691 setSettings(s => ({
1692 ...s,
1693 automation: {
1694 ...s.automation,
1695 queuePriority: e.target.value
1696 }
1697 }))
1698 }
1699 >
1700 <option value="high">High</option>
1701 <option value="normal">Normal</option>
1702 <option value="low">Low</option>
1703 </select>
1704 </div>
1705
1706 <div className="form-group">
1707 <label>Auto-Retry Failed Jobs</label>
1708 <input
1709 id="automation-auto-retry"
1710 name="automation-auto-retry"
1711 type="checkbox"
1712 checked={settings.automation?.autoRetry ?? true}
1713 onChange={(e) =>
1714 setSettings(s => ({
1715 ...s,
1716 automation: {
1717 ...s.automation,
1718 autoRetry: e.target.checked
1719 }
1720 }))
1721 }
1722 />
1723 </div>
1724
1725 {/* Automation Alerts */}
1726 <div className="form-group">
1727 <label>Alert: Agent Offline</label>
1728 <input
1729 id="alert-agent-offline"
1730 name="alert-agent-offline"
1731 type="checkbox"
1732 checked={settings.automation?.alerts?.agentOffline ?? true}
1733 onChange={(e) =>
1734 setSettings(s => ({
1735 ...s,
1736 automation: {
1737 ...s.automation,
1738 alerts: {
1739 ...s.automation?.alerts,
1740 agentOffline: e.target.checked
1741 }
1742 }
1743 }))
1744 }
1745 />
1746 </div>
1747
1748 <div className="form-group">
1749 <label>Alert: Contract Expiry</label>
1750 <input
1751 id="alert-contract-expiry"
1752 name="alert-contract-expiry"
1753 type="checkbox"
1754 checked={settings.automation?.alerts?.contractExpiry ?? true}
1755 onChange={(e) =>
1756 setSettings(s => ({
1757 ...s,
1758 automation: {
1759 ...s.automation,
1760 alerts: {
1761 ...s.automation?.alerts,
1762 contractExpiry: e.target.checked
1763 }
1764 }
1765 }))
1766 }
1767 />
1768 </div>
1769
1770 <div className="form-group">
1771 <label>Alert: Ticket Idle (hours)</label>
1772 <input
1773 id="alert-ticket-idle"
1774 name="alert-ticket-idle"
1775 type="number"
1776 min={1}
1777 value={settings.automation?.alerts?.ticketIdleHours ?? 24}
1778 onChange={(e) =>
1779 setSettings(s => ({
1780 ...s,
1781 automation: {
1782 ...s.automation,
1783 alerts: {
1784 ...s.automation?.alerts,
1785 ticketIdleHours: parseInt(e.target.value || 24, 10)
1786 }
1787 }
1788 }))
1789 }
1790 />
1791 </div>
1792
1793 <div className="form-group">
1794 <label>Alert: Auto-Escalate Ticket (hours)</label>
1795 <input
1796 id="alert-auto-escalate"
1797 name="alert-auto-escalate"
1798 type="number"
1799 min={1}
1800 value={settings.automation?.alerts?.autoEscalateHours ?? 24}
1801 onChange={(e) =>
1802 setSettings(s => ({
1803 ...s,
1804 automation: {
1805 ...s.automation,
1806 alerts: {
1807 ...s.automation?.alerts,
1808 autoEscalateHours: parseInt(e.target.value || 24, 10)
1809 }
1810 }
1811 }))
1812 }
1813 />
1814 </div>
1815
1816 <div className="form-group">
1817 <label>Enable AI Ticket Suggestions</label>
1818 <input
1819 id="automation-ai-suggestions"
1820 name="automation-ai-suggestions"
1821 type="checkbox"
1822 checked={settings.automation?.aiSuggestions ?? true}
1823 onChange={(e) =>
1824 setSettings(s => ({
1825 ...s,
1826 automation: {
1827 ...s.automation,
1828 aiSuggestions: e.target.checked
1829 }
1830 }))
1831 }
1832 />
1833 </div>
1834
1835 <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
1836 Configure automation, alert rules, and queued job behaviour for this tenant.
1837 </div>
1838
1839 </div>
1840 </section>
1841
1842 {/* ------------------------------------------------ */}
1843 {/* 9. USER MANAGEMENT */}
1844 {/* ------------------------------------------------ */}
1845 <section className="settings-section full-width">
1846 <h2>
1847 <span className="material-symbols-outlined">group</span>
1848 User Management
1849 </h2>
1850
1851 <div className="settings-form">
1852
1853 {/* Search users */}
1854 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
1855 <div style={{ flex: 1 }}>
1856 <label style={{ display: 'block', marginBottom: 6 }}>Search users</label>
1857 <input
1858 className="user-search"
1859 id="user-search"
1860 name="user-search"
1861 type="text"
1862 placeholder="Search by name or email"
1863 value={searchTerm}
1864 onChange={(e) => setSearchTerm(e.target.value)}
1865 />
1866 </div>
1867 </div>
1868
1869 <div style={{ marginTop: 8 }}>
1870 {usersLoading ? (
1871 <div>Loading users...</div>
1872 ) : userError ? (
1873 <div className="error">{userError}</div>
1874 ) : (
1875 <table className="data-table">
1876 <thead>
1877 <tr><th>Name</th><th>Email</th><th>Role</th><th>Actions</th></tr>
1878 </thead>
1879 <tbody>
1880 {users.map(u => (
1881 <tr key={u.user_id}>
1882 <td>{u.name}</td>
1883 <td>{u.email}</td>
1884 <td>{u.role}</td>
1885 <td>
1886 <div className="action-buttons">
1887 {currentRole === 'admin' && (
1888 <>
1889 {/* Edit user */}
1890 <button
1891 className="btn primary"
1892 onClick={() => startEditUser(u)}
1893 >
1894 Edit
1895 </button>
1896
1897 {/* Reset Password */}
1898 <button
1899 className="btn"
1900 onClick={async () => {
1901 if (!confirm('Send password reset link to this user?')) return;
1902 try {
1903 const token = localStorage.getItem('token');
1904 const res = await apiFetch(`/users/${u.user_id}/reset-password`, {
1905 method: 'POST',
1906 headers: { Authorization: `Bearer ${token}` }
1907 });
1908 if (!res.ok) throw new Error('Reset failed');
1909 alert('Password reset email triggered (if email service configured).');
1910 } catch (err) {
1911 alert('Reset failed: ' + (err.message || err));
1912 }
1913 }}
1914 >
1915 Reset Password
1916 </button>
1917
1918 {/* Delete user */}
1919 <button
1920 className="btn"
1921 onClick={async () => {
1922 if (!confirm('Delete user?')) return;
1923 try {
1924 const token = localStorage.getItem('token');
1925 const res = await apiFetch(`/users/${u.user_id}`, {
1926 method: 'DELETE',
1927 headers: { Authorization: `Bearer ${token}` }
1928 });
1929 if (!res.ok) throw new Error('Delete failed');
1930 fetchUsers();
1931 } catch (err) {
1932 alert('Delete failed: ' + (err.message || err));
1933 }
1934 }}
1935 >
1936 Delete
1937 </button>
1938 </>
1939 )}
1940 </div>
1941 </td>
1942 </tr>
1943 ))}
1944 </tbody>
1945 </table>
1946 )}
1947 </div>
1948
1949 {/* ------------------------------------------------ */}
1950 {/* Edit User Modal */}
1951 {/* ------------------------------------------------ */}
1952 {editingUser && (
1953 <div className="edit-user-modal">
1954 <div className="edit-user-modal-content">
1955 <h3>Edit User: {editingUser.name}</h3>
1956
1957 <div className="form-group">
1958 <label>Name</label>
1959 <input
1960 type="text"
1961 value={editUserData.name}
1962 onChange={(e) => setEditUserData(prev => ({ ...prev, name: e.target.value }))}
1963 placeholder="Full name"
1964 />
1965 </div>
1966
1967 <div className="form-group">
1968 <label>Email</label>
1969 <input
1970 type="email"
1971 value={editUserData.email}
1972 onChange={(e) => setEditUserData(prev => ({ ...prev, email: e.target.value }))}
1973 placeholder="user@example.com"
1974 />
1975 </div>
1976
1977 <div className="form-group">
1978 <label>Role</label>
1979 <select
1980 value={editUserData.role}
1981 onChange={(e) => setEditUserData(prev => ({ ...prev, role: e.target.value }))}
1982 >
1983 <option value="staff">Staff</option>
1984 <option value="admin">Admin</option>
1985 <option value="msp">MSP</option>
1986 </select>
1987 </div>
1988
1989 <div className="modal-actions">
1990 <button
1991 className="btn"
1992 onClick={cancelEditUser}
1993 disabled={updatingUser}
1994 >
1995 Cancel
1996 </button>
1997 <button
1998 className="btn primary"
1999 onClick={updateUser}
2000 disabled={updatingUser}
2001 >
2002 {updatingUser ? 'Updating...' : 'Save Changes'}
2003 </button>
2004 </div>
2005 </div>
2006 </div>
2007 )}
2008
2009 {/* ------------------------------------------------ */}
2010 {/* Create New User */}
2011 {/* ------------------------------------------------ */}
2012 <div style={{ marginTop: 20, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
2013 <h3>User Management</h3>
2014 {currentRole === 'admin' && (
2015 <button
2016 className="btn primary"
2017 onClick={openCreateUserModal}
2018 style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
2019 >
2020 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>add</span>
2021 Create New User
2022 </button>
2023 )}
2024 </div>
2025
2026 {!currentRole || currentRole !== 'admin' ? (
2027 <div style={{ color: '#666', fontStyle: 'italic' }}>
2028 Only admin users can create or delete users.
2029 </div>
2030 ) : null}
2031
2032 {/* ------------------------------------------------ */}
2033 {/* Create User Modal */}
2034 {/* ------------------------------------------------ */}
2035 {showCreateUserModal && (
2036 <div className="edit-user-modal">
2037 <div className="edit-user-modal-content">
2038 <h3>Create New User</h3>
2039
2040 {/* Tenant selector (Root only) */}
2041 {isRootUser && (
2042 <div className="form-group" style={{ position: 'relative' }}>
2043 <label>Tenant</label>
2044
2045 <input
2046 id="tenant-search"
2047 name="tenant-search"
2048 type="text"
2049 placeholder="Search and select tenant..."
2050 value={
2051 selectedTenant
2052 ? `${selectedTenant.name} (${selectedTenant.subdomain})`
2053 : tenantSearch
2054 }
2055 onChange={(e) => {
2056 setTenantSearch(e.target.value);
2057 setSelectedTenant(null);
2058 setNewUser(n => ({ ...n, tenant_id: null }));
2059 setTenantDropdownOpen(true);
2060 }}
2061 onFocus={() => setTenantDropdownOpen(true)}
2062 onBlur={() => setTimeout(() => setTenantDropdownOpen(false), 200)}
2063 />
2064
2065 {/* Dropdown */}
2066 {tenantDropdownOpen && filteredTenants.length > 0 && (
2067 <div
2068 style={{
2069 position: 'absolute',
2070 top: '100%',
2071 left: 0,
2072 right: 0,
2073 maxHeight: '200px',
2074 overflowY: 'auto',
2075 backgroundColor: 'white',
2076 border: '1px solid #ddd',
2077 borderRadius: '4px',
2078 marginTop: '4px',
2079 zIndex: 1000,
2080 boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
2081 }}
2082 >
2083 {filteredTenants.map(t => (
2084 <div
2085 key={t.tenant_id}
2086 style={{
2087 padding: '8px 12px',
2088 cursor: 'pointer',
2089 borderBottom: '1px solid #f0f0f0',
2090 backgroundColor:
2091 selectedTenant?.tenant_id === t.tenant_id ? '#f0f0f0' : 'white'
2092 }}
2093 onMouseEnter={(e) => (e.target.style.backgroundColor = '#f5f5f5')}
2094 onMouseLeave={(e) =>
2095 (e.target.style.backgroundColor =
2096 selectedTenant?.tenant_id === t.tenant_id ? '#f0f0f0' : 'white')
2097 }
2098 onClick={() => {
2099 setSelectedTenant(t);
2100 setNewUser(n => ({ ...n, tenant_id: t.tenant_id }));
2101 setTenantSearch('');
2102 setTenantDropdownOpen(false);
2103 }}
2104 >
2105 <div style={{ fontWeight: 500 }}>{t.name}</div>
2106 <div style={{ fontSize: '0.85em', color: '#666' }}>{t.name}</div>
2107 </div>
2108 ))}
2109 </div>
2110 )}
2111
2112 <small style={{ display: 'block', marginTop: 4, color: '#666' }}>
2113 As a root user, you can create users in any tenant.
2114 Leave blank to use your current tenant.
2115 </small>
2116 </div>
2117 )}
2118
2119 {/* New user fields */}
2120 <div className="form-group">
2121 <label>Name</label>
2122 <input
2123 id="new-user-name"
2124 name="new-user-name"
2125 type="text"
2126 value={newUser.name}
2127 onChange={(e) =>
2128 setNewUser(n => ({ ...n, name: e.target.value }))
2129 }
2130 placeholder="Full name"
2131 />
2132 </div>
2133
2134 <div className="form-group">
2135 <label>Email</label>
2136 <input
2137 id="new-user-email"
2138 name="new-user-email"
2139 type="email"
2140 value={newUser.email}
2141 onChange={(e) =>
2142 setNewUser(n => ({ ...n, email: e.target.value }))
2143 }
2144 placeholder="user@example.com"
2145 />
2146 </div>
2147
2148 <div className="form-group">
2149 <label>Role</label>
2150 <select
2151 value={newUser.role}
2152 onChange={(e) =>
2153 setNewUser(n => ({ ...n, role: e.target.value }))
2154 }
2155 >
2156 <option value="staff">Staff</option>
2157 <option value="admin">Admin</option>
2158 <option value="msp">MSP</option>
2159 </select>
2160 </div>
2161
2162 <div className="form-group">
2163 <label>Password</label>
2164 <input
2165 id="new-user-password"
2166 name="new-user-password"
2167 type="password"
2168 value={newUser.password}
2169 onChange={(e) =>
2170 setNewUser(n => ({ ...n, password: e.target.value }))
2171 }
2172 placeholder="Enter password"
2173 />
2174 </div>
2175
2176 <div className="modal-actions">
2177 <button
2178 className="btn"
2179 onClick={closeCreateUserModal}
2180 disabled={creatingUser}
2181 >
2182 Cancel
2183 </button>
2184 <button
2185 className="btn primary"
2186 onClick={async () => {
2187 if (!newUser.name || !newUser.email || !newUser.password) {
2188 alert('Please fill name, email and password');
2189 return;
2190 }
2191 setCreatingUser(true);
2192 try {
2193 const token = localStorage.getItem('token');
2194 const res = await apiFetch('/users/register', {
2195 method: 'POST',
2196 headers: {
2197 'Content-Type': 'application/json',
2198 Authorization: `Bearer ${token}`
2199 },
2200 body: JSON.stringify(newUser)
2201 });
2202
2203 if (!res.ok) {
2204 const txt = await res.text();
2205 throw new Error(txt || 'Create user failed');
2206 }
2207
2208 const created = await res.json();
2209
2210 closeCreateUserModal();
2211 fetchUsers();
2212 alert('User created: ' + created.email);
2213
2214 } catch (err) {
2215 alert('Create user failed: ' + (err.message || err));
2216 } finally {
2217 setCreatingUser(false);
2218 }
2219 }}
2220 disabled={creatingUser}
2221 >
2222 {creatingUser ? 'Creating...' : 'Create User'}
2223 </button>
2224 </div>
2225 </div>
2226 </div>
2227 )}
2228 </div>
2229 </section>
2230
2231
2232 </div>
2233 )}
2234 </div>
2235 </MainLayout>
2236 );
2237}
2238
2239export default Settings;