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';
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();
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);
28 const [taxRate, setTaxRate] = useState(10);
29 const [taxRateLoading, setTaxRateLoading] = useState(true);
31 const [invoiceAdvanceDays, setInvoiceAdvanceDays] = useState(14);
32 const [invoiceAdvanceDaysLoading, setInvoiceAdvanceDaysLoading] = useState(true);
34 const [companySettings, setCompanySettings] = useState({
40 const [companySettingsLoading, setCompanySettingsLoading] = useState(true);
42 const [users, setUsers] = useState([]);
43 const [usersLoading, setUsersLoading] = useState(true);
44 const [userError, setUserError] = useState(null);
45 const [searchTerm, setSearchTerm] = useState('');
47 const [newUser, setNewUser] = useState({
54 const [creatingUser, setCreatingUser] = useState(false);
57 const [editingUser, setEditingUser] = useState(null);
58 const [editUserData, setEditUserData] = useState({
63 const [updatingUser, setUpdatingUser] = useState(false);
65 // Create user modal state
66 const [showCreateUserModal, setShowCreateUserModal] = useState(false);
68 const [currentRole, setCurrentRole] = useState(null);
69 const [isRootUser, setIsRootUser] = useState(false);
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('');
78 // -------------------------------
79 // WordPress Settings State (Root Tenant Only)
80 // -------------------------------
81 const [wordpressSettings, setWordpressSettings] = useState({
82 database_password: '',
83 spaces_access_key: '',
86 const [wordpressLoading, setWordpressLoading] = useState(false);
87 const [wordpressSaving, setWordpressSaving] = useState(false);
88 const [wordpressError, setWordpressError] = useState(null);
89 const [wordpressSuccess, setWordpressSuccess] = useState(false);
91 // -------------------------------
92 // Pax8 Integration State
93 // -------------------------------
94 const [pax8Settings, setPax8Settings] = useState({
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);
107 // -------------------------------
109 // -------------------------------
111 if (!selectedTenantId) return;
113 apiFetch(`/tenants/${selectedTenantId}/icon`)
114 .then(res => res.ok ? res.blob() : null)
116 if (blob) setIconPreview(URL.createObjectURL(blob));
117 else setIconPreview(null);
119 }, [selectedTenantId]);
121 // -------------------------------
123 // -------------------------------
125 if (!iconPreview) return;
127 let link = document.querySelector("link[rel~='icon']");
129 link = document.createElement('link');
131 document.head.appendChild(link);
133 link.href = iconPreview;
136 link.href = '/favicon.ico';
140 // -------------------------------
142 // -------------------------------
143 const handleIconUpload = async (e) => {
144 setIconUploading(true);
147 const file = e.target.files[0];
150 const formData = new FormData();
151 formData.append('icon', file);
154 const token = localStorage.getItem('token');
156 const res = await apiFetch(`/tenants/${selectedTenantId}/icon`, {
158 headers: { Authorization: `Bearer ${token}` },
162 if (!res.ok) throw new Error('Upload failed');
164 setIconPreview(URL.createObjectURL(file));
166 setIconError('Upload failed');
168 setIconUploading(false);
172 // -------------------------------
173 // Tenant-aware Headers
174 // -------------------------------
175 const getTenantHeaders = async (base = {}) => {
176 const token = localStorage.getItem('token');
177 if (!token) return base;
179 const headers = { ...base };
182 const payload = JSON.parse(atob(token.split('.')[1]));
183 const isMSP = payload.role === 'admin' || payload.role === 'msp';
185 if (isMSP && selectedTenantId) {
186 // No more subdomain header — JWT handles tenancy now
193 // -------------------------------
195 // -------------------------------
196 const fetchUsers = async (search) => {
197 setUsersLoading(true);
199 const token = localStorage.getItem('token');
201 if (search?.trim()) url += `?search=${encodeURIComponent(search.trim())}`;
203 const res = await apiFetch(url, {
204 headers: { Authorization: `Bearer ${token}` }
207 if (!res.ok) throw new Error('Failed to load users');
209 setUsers(await res.json());
212 setUserError(err.message || 'Error');
214 setUsersLoading(false);
218 // -------------------------------
219 // Fetch Settings, Tax, Company Info
220 // -------------------------------
222 const fetchAll = async () => {
223 // ---- General Settings ----
226 const token = localStorage.getItem('token');
227 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
229 const res = await apiFetch('/settings', { headers });
230 if (!res.ok) throw new Error('Failed to load settings');
232 const data = await res.json();
234 localStorage.setItem('app_settings', JSON.stringify(data));
237 setError(err.message);
243 setTaxRateLoading(true);
245 const token = localStorage.getItem('token');
246 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
248 const res = await apiFetch('/settings/default_tax_rate', { headers });
250 const data = await res.json();
251 setTaxRate(parseFloat(data.setting_value) || 10);
254 setTaxRateLoading(false);
257 // ---- Invoice Advance Days ----
258 setInvoiceAdvanceDaysLoading(true);
260 const token = localStorage.getItem('token');
261 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
263 const res = await apiFetch('/settings/invoice_advance_days', { headers });
265 const data = await res.json();
266 setInvoiceAdvanceDays(parseInt(data.setting_value, 10) || 14);
269 setInvoiceAdvanceDaysLoading(false);
272 // ---- Company Info ----
273 setCompanySettingsLoading(true);
275 const token = localStorage.getItem('token');
276 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
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 })
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 : ''
292 setCompanySettingsLoading(false);
297 }, [selectedTenantId]);
299 // -------------------------------
300 // Initial Load: Role, Root, Tenants, Users
301 // -------------------------------
303 const token = localStorage.getItem('token');
308 const payload = JSON.parse(atob(token.split('.')[1]));
309 setCurrentRole(payload.role);
313 // Root check + Tenants
314 const checkRoot = async () => {
318 const res = await apiFetch('/tenants', {
319 headers: { Authorization: `Bearer ${token}` }
322 setIsRootUser(res.ok);
325 const data = await res.json();
326 setTenants(data.tenants || []);
327 setFilteredTenants(data.tenants || []);
330 setIsRootUser(false);
338 // -------------------------------
339 // Fetch WordPress Settings (Root Tenant Only)
340 // -------------------------------
341 const fetchWordPressSettings = async () => {
342 if (!isRootUser) return; // Only fetch if root user
344 setWordpressLoading(true);
345 setWordpressError(null);
347 const token = localStorage.getItem('token');
348 const res = await apiFetch('/settings/wordpress', {
349 headers: { Authorization: `Bearer ${token}` }
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');
359 throw new Error('Failed to load WordPress settings');
362 console.error('Error fetching WordPress settings:', err);
363 setWordpressError(err.message || 'Error loading WordPress settings');
365 setWordpressLoading(false);
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');
378 setWordpressSaving(true);
379 setWordpressError(null);
380 setWordpressSuccess(false);
382 const token = localStorage.getItem('token');
383 const res = await apiFetch('/settings/wordpress', {
386 'Content-Type': 'application/json',
387 Authorization: `Bearer ${token}`
389 body: JSON.stringify(wordpressSettings)
393 const errorData = await res.json();
394 throw new Error(errorData.error || 'Failed to save WordPress settings');
397 setWordpressSuccess(true);
398 setTimeout(() => setWordpressSuccess(false), 3000);
400 console.error('Error saving WordPress settings:', err);
401 setWordpressError(err.message || 'Error saving WordPress settings');
403 setWordpressSaving(false);
409 const t = setTimeout(() => fetchUsers(searchTerm), 300);
410 return () => clearTimeout(t);
413 // Tenant search filter
415 if (!tenantSearch.trim()) {
416 setFilteredTenants(tenants);
418 const s = tenantSearch.toLowerCase();
420 tenants.filter(t => t.name.toLowerCase().includes(s))
423 }, [tenantSearch, tenants]);
425 // Fetch WordPress settings when root user status is confirmed
428 fetchWordPressSettings();
432 // -------------------------------
433 // Pax8 Integration Functions
434 // -------------------------------
435 const fetchPax8Config = async () => {
436 setPax8Loading(true);
439 const token = localStorage.getItem('token');
440 const res = await apiFetch('/office365/config', {
441 headers: { Authorization: `Bearer ${token}` }
445 const data = await res.json();
446 if (data.configured) {
447 setPax8Configured(true);
449 client_id: data.config.client_id || '',
450 client_secret: '', // Never send secret back
451 pax8_company_id: data.config.pax8_company_id || ''
454 setPax8Configured(false);
458 console.error('Error fetching Pax8 config:', err);
459 setPax8Error(err.message || 'Error loading Pax8 configuration');
461 setPax8Loading(false);
465 const savePax8Config = async () => {
466 if (!pax8Settings.client_id || !pax8Settings.client_secret) {
467 setPax8Error('Client ID and Client Secret are required');
473 setPax8Success(false);
475 const token = localStorage.getItem('token');
476 const res = await apiFetch('/office365/config', {
479 'Content-Type': 'application/json',
480 Authorization: `Bearer ${token}`
482 body: JSON.stringify(pax8Settings)
486 const errorData = await res.json();
487 throw new Error(errorData.error || 'Failed to save Pax8 configuration');
490 setPax8Success(true);
491 setPax8Configured(true);
492 setTimeout(() => setPax8Success(false), 3000);
494 console.error('Error saving Pax8 config:', err);
495 setPax8Error(err.message || 'Error saving Pax8 configuration');
497 setPax8Saving(false);
501 const testPax8Connection = async () => {
502 setPax8Testing(true);
503 setPax8TestResult(null);
506 const token = localStorage.getItem('token');
507 const res = await apiFetch('/office365/config/test', {
509 headers: { Authorization: `Bearer ${token}` }
512 const data = await res.json();
513 if (res.ok && data.success) {
514 setPax8TestResult({ success: true, message: 'Connection successful! Pax8 API is responding.' });
516 setPax8TestResult({ success: false, message: data.error || 'Connection test failed' });
519 console.error('Error testing Pax8 connection:', err);
520 setPax8TestResult({ success: false, message: err.message || 'Connection test failed' });
522 setPax8Testing(false);
526 // Fetch Pax8 config on load
529 }, [selectedTenantId]);
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');
540 setUpdatingUser(true);
542 const token = localStorage.getItem('token');
543 const res = await apiFetch(`/users/${editingUser.user_id}`, {
546 'Content-Type': 'application/json',
547 Authorization: `Bearer ${token}`
549 body: JSON.stringify({
550 name: editUserData.name,
551 email: editUserData.email,
552 role: editUserData.role
557 const errorText = await res.text();
558 throw new Error(errorText || 'Update failed');
561 // Refresh users list
565 setEditingUser(null);
566 setEditUserData({ name: '', email: '', role: '' });
568 alert('User updated successfully');
570 alert('Failed to update user: ' + (err.message || err));
572 setUpdatingUser(false);
576 const startEditUser = (user) => {
577 setEditingUser(user);
585 const cancelEditUser = () => {
586 setEditingUser(null);
587 setEditUserData({ name: '', email: '', role: '' });
590 const openCreateUserModal = () => {
591 setShowCreateUserModal(true);
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);
597 setSelectedTenant(currentTenant);
603 tenant_id: currentTenant.tenant_id
610 // Default: no tenant pre-selected
619 setSelectedTenant(null);
622 const closeCreateUserModal = () => {
623 setShowCreateUserModal(false);
632 setSelectedTenant(null);
635 // -------------------------------
637 // -------------------------------
640 <div className="settings-page">
647 backgroundColor: 'var(--warning-bg, #fff3cd)',
648 border: '1px solid var(--warning, #ffc107)',
650 marginBottom: '20px',
651 color: 'var(--warning-text, #856404)'
653 <strong>⚠️ Read-Only Access:</strong> Staff users can view settings but cannot make changes. Only admin users can modify settings.
657 {loading && <div>Loading...</div>}
658 {error && <div className="error">Error: {error}</div>}
661 <div className="settings-sections" style={{
662 opacity: !isAdmin() ? 0.7 : 1,
663 pointerEvents: !isAdmin() ? 'none' : 'auto',
664 userSelect: !isAdmin() ? 'none' : 'auto'
667 {/* ------------------------------------------------ */}
668 {/* 1. EMAIL AUTOMATION */}
669 {/* ------------------------------------------------ */}
670 <section className="settings-section">
671 <h2>Email Automation</h2>
673 <div className="settings-form">
675 <div className="form-group">
676 <label>Email Server Type</label>
678 value={settings.emailAutomation?.serverType ?? 'imap'}
679 onChange={e => setSettings(s => ({
682 ...s.emailAutomation,
683 serverType: e.target.value
687 <option value="imap">IMAP</option>
688 <option value="smtp">SMTP</option>
692 <div className="form-group">
693 <label>Server Host</label>
696 value={settings.emailAutomation?.host ?? ''}
697 onChange={e => setSettings(s => ({
700 ...s.emailAutomation,
704 placeholder="mail.example.com"
708 <div className="form-group">
709 <label>Server Port</label>
712 value={settings.emailAutomation?.port ?? 993}
713 onChange={e => setSettings(s => ({
716 ...s.emailAutomation,
717 port: parseInt(e.target.value || '993', 10)
724 <div className="form-group">
725 <label>Mailbox (e.g. INBOX)</label>
728 value={settings.emailAutomation?.mailbox ?? 'INBOX'}
729 onChange={e => setSettings(s => ({
732 ...s.emailAutomation,
733 mailbox: e.target.value
740 <div className="form-group">
741 <label>Email Username</label>
744 value={settings.emailAutomation?.username ?? ''}
745 onChange={e => setSettings(s => ({
748 ...s.emailAutomation,
749 username: e.target.value
752 placeholder="user@example.com"
756 <div className="form-group">
757 <label>Email Password</label>
760 value={settings.emailAutomation?.password ?? ''}
761 onChange={e => setSettings(s => ({
764 ...s.emailAutomation,
765 password: e.target.value
768 placeholder="••••••••"
772 <div className="form-group">
773 <label>Connection Security</label>
775 value={settings.emailAutomation?.security ?? 'ssl'}
776 onChange={e => setSettings(s => ({
779 ...s.emailAutomation,
780 security: e.target.value
784 <option value="ssl">SSL/TLS</option>
785 <option value="starttls">STARTTLS</option>
786 <option value="none">None</option>
790 <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
791 Configure email server connection for ticket automation. Credentials are stored securely per tenant.
796 {/* ------------------------------------------------ */}
797 {/* 2. COMPANY INFORMATION */}
798 {/* ------------------------------------------------ */}
799 <section className="settings-section">
800 <h2>Company Information (for Invoices)</h2>
802 <div className="settings-form">
804 <div className="form-group">
805 <label>Company ABN</label>
810 value={companySettings.abn}
811 onChange={(e) => setCompanySettings(s => ({
815 disabled={companySettingsLoading}
816 placeholder="12 345 678 901"
818 <small style={{ display: 'block', marginTop: '4px', color: '#666' }}>
819 Your Australian Business Number (appears on invoices)
823 <div className="form-group">
824 <label>Business Address</label>
827 name="company-address"
829 value={companySettings.address}
830 onChange={(e) => setCompanySettings(s => ({
832 address: e.target.value
834 disabled={companySettingsLoading}
835 placeholder="123 Business St, City, State, Postcode"
839 <div className="form-group">
840 <label>Business Phone</label>
845 value={companySettings.phone}
846 onChange={(e) => setCompanySettings(s => ({
848 phone: e.target.value
850 disabled={companySettingsLoading}
851 placeholder="(02) 1234 5678"
855 <div className="form-group">
856 <label>Business Email</label>
861 value={companySettings.email}
862 onChange={(e) => setCompanySettings(s => ({
864 email: e.target.value
866 disabled={companySettingsLoading}
867 placeholder="info@yourcompany.com.au"
872 className="btn primary"
873 onClick={async () => {
875 const token = localStorage.getItem('token');
876 const headers = await getTenantHeaders({
877 'Content-Type': 'application/json',
878 Authorization: `Bearer ${token}`
882 apiFetch('/settings/company_abn', {
885 body: JSON.stringify({
886 setting_value: companySettings.abn,
887 description: 'Company ABN'
891 apiFetch('/settings/company_address', {
894 body: JSON.stringify({
895 setting_value: companySettings.address,
896 description: 'Company Address'
900 apiFetch('/settings/company_phone', {
903 body: JSON.stringify({
904 setting_value: companySettings.phone,
905 description: 'Company Phone'
909 apiFetch('/settings/company_email', {
912 body: JSON.stringify({
913 setting_value: companySettings.email,
914 description: 'Company Email'
919 alert('Company information updated successfully');
922 console.error('Failed to update company information:', err);
923 alert('Failed to update company information: ' + err.message);
927 Save Company Information
933 {/* ------------------------------------------------ */}
934 {/* 3. TAX SETTINGS */}
935 {/* ------------------------------------------------ */}
936 {/* 3. BILLING & INVOICING SETTINGS */}
937 {/* ------------------------------------------------ */}
938 <section className="settings-section">
939 <h2>Billing & Invoicing</h2>
941 <div className="settings-form">
943 <div className="form-group">
944 <label>Default Tax Rate (GST %)</label>
946 id="company-tax-rate"
947 name="company-tax-rate"
952 setTaxRate(parseFloat(e.target.value) || 0)
954 disabled={taxRateLoading}
961 Australia GST is 10%. This rate will be applied to all new invoices.
965 <div className="form-group">
966 <label>Invoice Advance Days</label>
968 id="invoice-advance-days"
969 name="invoice-advance-days"
974 value={invoiceAdvanceDays}
976 setInvoiceAdvanceDays(parseInt(e.target.value, 10) || 0)
978 disabled={invoiceAdvanceDaysLoading}
985 Generate invoices this many days before renewal date. Default is 14 days. Applies to all contracts with auto-invoicing enabled.
990 className="btn primary"
991 onClick={async () => {
993 const token = localStorage.getItem('token');
994 const headers = await getTenantHeaders({
995 'Content-Type': 'application/json',
996 Authorization: `Bearer ${token}`
1000 apiFetch('/settings/default_tax_rate', {
1003 body: JSON.stringify({
1004 setting_value: taxRate.toString(),
1005 description: 'Default GST/Tax rate percentage'
1008 apiFetch('/settings/invoice_advance_days', {
1011 body: JSON.stringify({
1012 setting_value: invoiceAdvanceDays.toString(),
1013 description: 'Days before renewal to generate invoice'
1018 alert('Billing settings updated successfully');
1021 alert('Failed to update billing settings: ' + err.message);
1025 Save Billing Settings
1031 {/* ------------------------------------------------ */}
1032 {/* 4. GENERAL SETTINGS */}
1033 {/* ------------------------------------------------ */}
1034 <section className="settings-section">
1035 <h2>General Settings</h2>
1037 <div className="settings-form">
1039 <div className="form-group">
1040 <label>Company Name</label>
1045 value={settings.general.companyName}
1051 companyName: e.target.value
1058 <div className="form-group">
1059 <label>Timezone</label>
1061 id="company-timezone"
1062 name="company-timezone"
1064 value={settings.general.timezone}
1070 timezone: e.target.value
1077 <div className="form-group">
1078 <label>Date Format</label>
1080 id="company-date-format"
1081 name="company-date-format"
1083 value={settings.general.dateFormat}
1089 dateFormat: e.target.value
1097 className="btn primary"
1098 onClick={async () => {
1101 const token = localStorage.getItem('token');
1102 const headers = await getTenantHeaders({
1103 'Content-Type': 'application/json',
1104 Authorization: `Bearer ${token}`
1107 const res = await apiFetch('/settings', {
1110 body: JSON.stringify(settings)
1114 const txt = await res.text();
1115 throw new Error(txt || 'Failed to save settings');
1118 const saved = await res.json();
1121 localStorage.setItem('app_settings', JSON.stringify(saved));
1125 applyThemeFromSettings(saved);
1128 window.dispatchEvent(
1129 new CustomEvent('settingsUpdated', { detail: saved })
1132 await notifySuccess('Settings Saved', 'General settings have been saved successfully');
1135 await notifyError('Save Failed', err.message || 'Failed to save settings');
1142 {saving ? 'Saving...' : 'Save General Settings'}
1148 {/* ------------------------------------------------ */}
1149 {/* 5. WHITE LABEL BRANDING */}
1150 {/* ------------------------------------------------ */}
1151 <section className="settings-section">
1152 <h2>White Label Branding</h2>
1154 <div className="settings-form">
1156 <div className="form-group">
1157 <label>Company Icon (for installer & dashboard)</label>
1161 accept="image/png,image/x-icon"
1163 onChange={handleIconUpload}
1164 disabled={iconUploading}
1168 <div style={{ marginTop: 12 }}>
1176 boxShadow: '0 0 4px #ccc'
1191 {iconUploading && <div>Uploading...</div>}
1192 {iconError && <div className="error">{iconError}</div>}
1201 This icon will be used for your installer and dashboard browser tab.
1208 {/* ------------------------------------------------ */}
1209 {/* 6. WORDPRESS SETTINGS (Root Tenant Only) */}
1210 {/* ------------------------------------------------ */}
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.
1219 <div className="settings-form">
1220 <div className="form-group">
1221 <label htmlFor="wp-db-password">Database Password *</label>
1225 value={wordpressSettings.database_password}
1226 onChange={(e) => setWordpressSettings(prev => ({
1228 database_password: e.target.value
1230 placeholder="Strong database password"
1231 disabled={wordpressLoading}
1233 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1234 MySQL database password for all WordPress sites
1238 <div className="form-group">
1239 <label htmlFor="wp-spaces-key">DO Spaces Access Key *</label>
1243 value={wordpressSettings.spaces_access_key}
1244 onChange={(e) => setWordpressSettings(prev => ({
1246 spaces_access_key: e.target.value
1248 placeholder="DO00..."
1249 disabled={wordpressLoading}
1251 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1252 DigitalOcean Spaces access key for media storage
1256 <div className="form-group">
1257 <label htmlFor="wp-spaces-secret">DO Spaces Secret Key *</label>
1259 id="wp-spaces-secret"
1261 value={wordpressSettings.spaces_secret_key}
1262 onChange={(e) => setWordpressSettings(prev => ({
1264 spaces_secret_key: e.target.value
1266 placeholder="Secret key"
1267 disabled={wordpressLoading}
1269 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1270 DigitalOcean Spaces secret key for media storage
1274 {wordpressError && (
1275 <div style={{ padding: '12px', background: '#fee', border: '1px solid #fcc', borderRadius: '4px', color: '#c33', marginBottom: '12px' }}>
1280 {wordpressSuccess && (
1281 <div style={{ padding: '12px', background: '#efe', border: '1px solid #cfc', borderRadius: '4px', color: '#3c3', marginBottom: '12px' }}>
1282 WordPress settings saved successfully!
1287 className="btn primary"
1288 onClick={saveWordPressSettings}
1289 disabled={wordpressSaving || wordpressLoading}
1291 {wordpressSaving ? 'Saving...' : 'Save WordPress Settings'}
1297 {/* ------------------------------------------------ */}
1298 {/* 7. PAX8 MICROSOFT 365 INTEGRATION */}
1299 {/* ------------------------------------------------ */}
1300 <section className="settings-section">
1301 <h2>Pax8 Microsoft 365 Integration</h2>
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' }}>
1312 <div className="settings-form">
1313 <div className="form-group">
1314 <label htmlFor="pax8-client-id">Pax8 Client ID *</label>
1318 value={pax8Settings.client_id}
1319 onChange={(e) => setPax8Settings(prev => ({
1321 client_id: e.target.value
1323 placeholder="Enter Pax8 OAuth client ID"
1324 disabled={pax8Loading}
1326 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1327 OAuth 2.0 client ID from Pax8 partner portal
1331 <div className="form-group">
1332 <label htmlFor="pax8-client-secret">Pax8 Client Secret *</label>
1334 id="pax8-client-secret"
1336 value={pax8Settings.client_secret}
1337 onChange={(e) => setPax8Settings(prev => ({
1339 client_secret: e.target.value
1341 placeholder="Enter Pax8 OAuth client secret"
1342 disabled={pax8Loading}
1344 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1345 OAuth 2.0 client secret (will be encrypted when saved)
1349 <div className="form-group">
1350 <label htmlFor="pax8-company-id">Pax8 Company ID (Optional)</label>
1352 id="pax8-company-id"
1354 value={pax8Settings.pax8_company_id}
1355 onChange={(e) => setPax8Settings(prev => ({
1357 pax8_company_id: e.target.value
1359 placeholder="Your Pax8 company ID"
1360 disabled={pax8Loading}
1362 <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
1363 Your partner company ID in Pax8 (optional)
1368 <div style={{ padding: '12px', background: '#fee', border: '1px solid #fcc', borderRadius: '4px', color: '#c33', marginBottom: '12px' }}>
1374 <div style={{ padding: '12px', background: '#efe', border: '1px solid #cfc', borderRadius: '4px', color: '#3c3', marginBottom: '12px' }}>
1375 Pax8 configuration saved successfully!
1379 {pax8TestResult && (
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'
1388 {pax8TestResult.message}
1392 <div style={{ display: 'flex', gap: '12px' }}>
1394 className="btn primary"
1395 onClick={savePax8Config}
1396 disabled={pax8Saving || pax8Loading}
1398 {pax8Saving ? 'Saving...' : 'Save Configuration'}
1403 onClick={testPax8Connection}
1404 disabled={pax8Testing || !pax8Configured}
1405 title={!pax8Configured ? 'Save configuration first' : 'Test connection to Pax8 API'}
1407 {pax8Testing ? 'Testing...' : 'Test Connection'}
1413 {/* ------------------------------------------------ */}
1414 {/* 8. NOTIFICATION SETTINGS */}
1415 {/* ------------------------------------------------ */}
1416 <section className="settings-section">
1417 <h2>Notification Settings</h2>
1419 <div className="settings-form">
1421 <div className="form-group">
1422 <label>Email Notifications</label>
1424 id="notifications-email"
1425 name="notifications-email"
1427 checked={settings.notifications.emailEnabled}
1433 emailEnabled: e.target.checked
1440 <div className="form-group">
1441 <label>Slack Notifications</label>
1443 id="notifications-slack"
1444 name="notifications-slack"
1446 checked={settings.notifications.slackEnabled}
1452 slackEnabled: e.target.checked
1460 <h3>Alert Thresholds</h3>
1462 <div className="settings-form">
1464 <div className="form-group">
1465 <label>CPU Warning Level (%)</label>
1470 value={settings.notifications.alertThresholds.cpu}
1477 ...s.notifications.alertThresholds,
1478 cpu: parseInt(e.target.value || 0, 10)
1486 <div className="form-group">
1487 <label>Memory Warning Level (%)</label>
1492 value={settings.notifications.alertThresholds.memory}
1499 ...s.notifications.alertThresholds,
1500 memory: parseInt(e.target.value || 0, 10)
1508 <div className="form-group">
1509 <label>Disk Warning Level (%)</label>
1514 value={settings.notifications.alertThresholds.disk}
1521 ...s.notifications.alertThresholds,
1522 disk: parseInt(e.target.value || 0, 10)
1533 {/* ------------------------------------------------ */}
1534 {/* 7. DISPLAY SETTINGS */}
1535 {/* ------------------------------------------------ */}
1536 <section className="settings-section">
1537 <h2>Display Settings</h2>
1539 <div className="settings-form">
1541 <div className="form-group">
1542 <label>Items per Page</label>
1544 id="display-items-per-page"
1545 name="display-items-per-page"
1547 value={settings.display.itemsPerPage}
1553 itemsPerPage: parseInt(e.target.value || 10, 10)
1560 <div className="form-group">
1561 <label>Theme</label>
1563 value={settings.display.theme}
1569 theme: e.target.value
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>
1581 <div className="form-group">
1582 <label>Auto switch theme</label>
1584 id="display-auto-theme"
1585 name="display-auto-theme"
1587 checked={settings.display.autoTheme}
1593 autoTheme: e.target.checked
1602 <div style={{ marginTop: 12 }}>
1605 onClick={async () => {
1608 const token = localStorage.getItem('token');
1609 const headers = await getTenantHeaders({
1610 'Content-Type': 'application/json',
1611 Authorization: `Bearer ${token}`
1614 const res = await apiFetch('/settings', {
1617 body: JSON.stringify(settings)
1621 const txt = await res.text();
1622 throw new Error(txt || 'Failed to save settings');
1625 const saved = await res.json();
1629 localStorage.setItem('app_settings', JSON.stringify(saved));
1633 applyThemeFromSettings(saved);
1636 window.dispatchEvent(
1637 new CustomEvent('settingsUpdated', { detail: saved })
1640 await notifySuccess('Settings Saved', 'All settings have been saved successfully');
1643 await notifyError('Save Failed', err.message || 'Failed to save settings');
1649 {saving ? 'Saving...' : 'Save Settings'}
1656 {/* ------------------------------------------------ */}
1657 {/* 8. AUTOMATION & ALERTS */}
1658 {/* (This appears last in your requested order) */}
1659 {/* ------------------------------------------------ */}
1660 <section className="settings-section">
1661 <h2>Automation & Alerts</h2>
1663 <div className="settings-form">
1665 <div className="form-group">
1666 <label>Enable Automation</label>
1668 id="automation-enabled"
1669 name="automation-enabled"
1671 checked={settings.automation?.enabled ?? true}
1677 enabled: e.target.checked
1684 <div className="form-group">
1685 <label>Job Queue Priority</label>
1687 id="automation-queue-priority"
1688 name="automation-queue-priority"
1689 value={settings.automation?.queuePriority ?? 'normal'}
1695 queuePriority: e.target.value
1700 <option value="high">High</option>
1701 <option value="normal">Normal</option>
1702 <option value="low">Low</option>
1706 <div className="form-group">
1707 <label>Auto-Retry Failed Jobs</label>
1709 id="automation-auto-retry"
1710 name="automation-auto-retry"
1712 checked={settings.automation?.autoRetry ?? true}
1718 autoRetry: e.target.checked
1725 {/* Automation Alerts */}
1726 <div className="form-group">
1727 <label>Alert: Agent Offline</label>
1729 id="alert-agent-offline"
1730 name="alert-agent-offline"
1732 checked={settings.automation?.alerts?.agentOffline ?? true}
1739 ...s.automation?.alerts,
1740 agentOffline: e.target.checked
1748 <div className="form-group">
1749 <label>Alert: Contract Expiry</label>
1751 id="alert-contract-expiry"
1752 name="alert-contract-expiry"
1754 checked={settings.automation?.alerts?.contractExpiry ?? true}
1761 ...s.automation?.alerts,
1762 contractExpiry: e.target.checked
1770 <div className="form-group">
1771 <label>Alert: Ticket Idle (hours)</label>
1773 id="alert-ticket-idle"
1774 name="alert-ticket-idle"
1777 value={settings.automation?.alerts?.ticketIdleHours ?? 24}
1784 ...s.automation?.alerts,
1785 ticketIdleHours: parseInt(e.target.value || 24, 10)
1793 <div className="form-group">
1794 <label>Alert: Auto-Escalate Ticket (hours)</label>
1796 id="alert-auto-escalate"
1797 name="alert-auto-escalate"
1800 value={settings.automation?.alerts?.autoEscalateHours ?? 24}
1807 ...s.automation?.alerts,
1808 autoEscalateHours: parseInt(e.target.value || 24, 10)
1816 <div className="form-group">
1817 <label>Enable AI Ticket Suggestions</label>
1819 id="automation-ai-suggestions"
1820 name="automation-ai-suggestions"
1822 checked={settings.automation?.aiSuggestions ?? true}
1828 aiSuggestions: e.target.checked
1835 <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
1836 Configure automation, alert rules, and queued job behaviour for this tenant.
1842 {/* ------------------------------------------------ */}
1843 {/* 9. USER MANAGEMENT */}
1844 {/* ------------------------------------------------ */}
1845 <section className="settings-section full-width">
1847 <span className="material-symbols-outlined">group</span>
1851 <div className="settings-form">
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>
1858 className="user-search"
1862 placeholder="Search by name or email"
1864 onChange={(e) => setSearchTerm(e.target.value)}
1869 <div style={{ marginTop: 8 }}>
1871 <div>Loading users...</div>
1873 <div className="error">{userError}</div>
1875 <table className="data-table">
1877 <tr><th>Name</th><th>Email</th><th>Role</th><th>Actions</th></tr>
1881 <tr key={u.user_id}>
1886 <div className="action-buttons">
1887 {currentRole === 'admin' && (
1891 className="btn primary"
1892 onClick={() => startEditUser(u)}
1897 {/* Reset Password */}
1900 onClick={async () => {
1901 if (!confirm('Send password reset link to this user?')) return;
1903 const token = localStorage.getItem('token');
1904 const res = await apiFetch(`/users/${u.user_id}/reset-password`, {
1906 headers: { Authorization: `Bearer ${token}` }
1908 if (!res.ok) throw new Error('Reset failed');
1909 alert('Password reset email triggered (if email service configured).');
1911 alert('Reset failed: ' + (err.message || err));
1921 onClick={async () => {
1922 if (!confirm('Delete user?')) return;
1924 const token = localStorage.getItem('token');
1925 const res = await apiFetch(`/users/${u.user_id}`, {
1927 headers: { Authorization: `Bearer ${token}` }
1929 if (!res.ok) throw new Error('Delete failed');
1932 alert('Delete failed: ' + (err.message || err));
1949 {/* ------------------------------------------------ */}
1950 {/* Edit User Modal */}
1951 {/* ------------------------------------------------ */}
1953 <div className="edit-user-modal">
1954 <div className="edit-user-modal-content">
1955 <h3>Edit User: {editingUser.name}</h3>
1957 <div className="form-group">
1961 value={editUserData.name}
1962 onChange={(e) => setEditUserData(prev => ({ ...prev, name: e.target.value }))}
1963 placeholder="Full name"
1967 <div className="form-group">
1968 <label>Email</label>
1971 value={editUserData.email}
1972 onChange={(e) => setEditUserData(prev => ({ ...prev, email: e.target.value }))}
1973 placeholder="user@example.com"
1977 <div className="form-group">
1980 value={editUserData.role}
1981 onChange={(e) => setEditUserData(prev => ({ ...prev, role: e.target.value }))}
1983 <option value="staff">Staff</option>
1984 <option value="admin">Admin</option>
1985 <option value="msp">MSP</option>
1989 <div className="modal-actions">
1992 onClick={cancelEditUser}
1993 disabled={updatingUser}
1998 className="btn primary"
1999 onClick={updateUser}
2000 disabled={updatingUser}
2002 {updatingUser ? 'Updating...' : 'Save Changes'}
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' && (
2016 className="btn primary"
2017 onClick={openCreateUserModal}
2018 style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
2020 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>add</span>
2026 {!currentRole || currentRole !== 'admin' ? (
2027 <div style={{ color: '#666', fontStyle: 'italic' }}>
2028 Only admin users can create or delete users.
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>
2040 {/* Tenant selector (Root only) */}
2042 <div className="form-group" style={{ position: 'relative' }}>
2043 <label>Tenant</label>
2047 name="tenant-search"
2049 placeholder="Search and select tenant..."
2052 ? `${selectedTenant.name} (${selectedTenant.subdomain})`
2056 setTenantSearch(e.target.value);
2057 setSelectedTenant(null);
2058 setNewUser(n => ({ ...n, tenant_id: null }));
2059 setTenantDropdownOpen(true);
2061 onFocus={() => setTenantDropdownOpen(true)}
2062 onBlur={() => setTimeout(() => setTenantDropdownOpen(false), 200)}
2066 {tenantDropdownOpen && filteredTenants.length > 0 && (
2069 position: 'absolute',
2075 backgroundColor: 'white',
2076 border: '1px solid #ddd',
2077 borderRadius: '4px',
2080 boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
2083 {filteredTenants.map(t => (
2087 padding: '8px 12px',
2089 borderBottom: '1px solid #f0f0f0',
2091 selectedTenant?.tenant_id === t.tenant_id ? '#f0f0f0' : 'white'
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')
2099 setSelectedTenant(t);
2100 setNewUser(n => ({ ...n, tenant_id: t.tenant_id }));
2101 setTenantSearch('');
2102 setTenantDropdownOpen(false);
2105 <div style={{ fontWeight: 500 }}>{t.name}</div>
2106 <div style={{ fontSize: '0.85em', color: '#666' }}>{t.name}</div>
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.
2119 {/* New user fields */}
2120 <div className="form-group">
2124 name="new-user-name"
2126 value={newUser.name}
2128 setNewUser(n => ({ ...n, name: e.target.value }))
2130 placeholder="Full name"
2134 <div className="form-group">
2135 <label>Email</label>
2138 name="new-user-email"
2140 value={newUser.email}
2142 setNewUser(n => ({ ...n, email: e.target.value }))
2144 placeholder="user@example.com"
2148 <div className="form-group">
2151 value={newUser.role}
2153 setNewUser(n => ({ ...n, role: e.target.value }))
2156 <option value="staff">Staff</option>
2157 <option value="admin">Admin</option>
2158 <option value="msp">MSP</option>
2162 <div className="form-group">
2163 <label>Password</label>
2165 id="new-user-password"
2166 name="new-user-password"
2168 value={newUser.password}
2170 setNewUser(n => ({ ...n, password: e.target.value }))
2172 placeholder="Enter password"
2176 <div className="modal-actions">
2179 onClick={closeCreateUserModal}
2180 disabled={creatingUser}
2185 className="btn primary"
2186 onClick={async () => {
2187 if (!newUser.name || !newUser.email || !newUser.password) {
2188 alert('Please fill name, email and password');
2191 setCreatingUser(true);
2193 const token = localStorage.getItem('token');
2194 const res = await apiFetch('/users/register', {
2197 'Content-Type': 'application/json',
2198 Authorization: `Bearer ${token}`
2200 body: JSON.stringify(newUser)
2204 const txt = await res.text();
2205 throw new Error(txt || 'Create user failed');
2208 const created = await res.json();
2210 closeCreateUserModal();
2212 alert('User created: ' + created.email);
2215 alert('Create user failed: ' + (err.message || err));
2217 setCreatingUser(false);
2220 disabled={creatingUser}
2222 {creatingUser ? 'Creating...' : 'Create User'}
2239export default Settings;