1import { useEffect, useState } from 'react';
2// import removed: TenantSelector
3import { useNavigate } from 'react-router-dom';
4import MainLayout from '../components/Layout/MainLayout';
6import { apiFetch } from '../lib/api';
9 // Use tenantId from JWT
10 const token = localStorage.getItem('token');
14 const payload = token.split('.')[1];
15 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
16 tenantId = decoded.tenant_id || decoded.tenantId || '';
19 const navigate = useNavigate();
20 const [tenants, setTenants] = useState([]);
21 const [loading, setLoading] = useState(false);
22 const [error, setError] = useState('');
23 const [total, setTotal] = useState(0);
26 const [page, setPage] = useState(1);
27 const [limit] = useState(10);
30 const [search, setSearch] = useState('');
31 const [statusFilter, setStatusFilter] = useState('');
33 // Delete confirmation modal
34 const [showDeleteModal, setShowDeleteModal] = useState(false);
35 const [tenantToDelete, setTenantToDelete] = useState(null);
36 const [deleteRelatedData, setDeleteRelatedData] = useState(null);
37 const [forceDelete, setForceDelete] = useState(false);
38 const [deleting, setDeleting] = useState(false);
42 }, [page, search, statusFilter, tenantId]);
44 async function fetchTenants() {
48 const token = localStorage.getItem('token');
49 const params = new URLSearchParams({
53 if (search) params.append('search', search);
54 if (statusFilter) params.append('status', statusFilter);
55 // Only send tenant_id if not root tenant
56 if (tenantId && tenantId !== '00000000-0000-0000-0000-000000000001') {
57 params.append('tenant_id', tenantId);
60 const res = await apiFetch(`/tenants?${params}`, {
62 Authorization: `Bearer ${token}`,
63 'X-Tenant-Subdomain': 'admin'
68 if (res.status === 403) {
69 throw new Error('Access denied. Root tenant privileges required.');
71 throw new Error('Failed to load tenants');
74 const data = await res.json();
75 setTenants(data.tenants || []);
76 setTotal(data.total || data.tenants?.length || 0);
79 setError(err.message || 'Load error');
85 async function handleDelete(tenantId, tenantName) {
86 // First, check what data exists for this tenant
87 setTenantToDelete({ id: tenantId, name: tenantName });
91 const token = localStorage.getItem('token');
92 const res = await apiFetch(`/tenants/${tenantId}`, {
94 Authorization: `Bearer ${token}`,
95 'X-Tenant-Subdomain': 'admin'
100 const data = await res.json();
101 setDeleteRelatedData(data.stats);
102 setShowDeleteModal(true);
106 alert('Failed to check tenant data');
112 async function confirmDelete() {
113 if (!tenantToDelete) return;
117 const token = localStorage.getItem('token');
118 const url = forceDelete
119 ? `/tenants/${tenantToDelete.id}?force=true`
120 : `/tenants/${tenantToDelete.id}`;
122 const res = await apiFetch(url, {
125 Authorization: `Bearer ${token}`,
126 'X-Tenant-Subdomain': 'admin'
131 const data = await res.json();
133 // If there are related records preventing deletion, show detailed message
136 if (parseInt(data.details.user_count) > 0) counts.push(`${data.details.user_count} users`);
137 if (parseInt(data.details.customer_count) > 0) counts.push(`${data.details.customer_count} customers`);
138 if (parseInt(data.details.ticket_count) > 0) counts.push(`${data.details.ticket_count} tickets`);
139 if (parseInt(data.details.invoice_count) > 0) counts.push(`${data.details.invoice_count} invoices`);
140 if (parseInt(data.details.contract_count) > 0) counts.push(`${data.details.contract_count} contracts`);
141 if (parseInt(data.details.agent_count) > 0) counts.push(`${data.details.agent_count} agents`);
142 if (parseInt(data.details.domain_count) > 0) counts.push(`${data.details.domain_count} domains`);
143 if (parseInt(data.details.purchase_order_count) > 0) counts.push(`${data.details.purchase_order_count} purchase orders`);
145 const message = counts.length > 0
146 ? `Cannot delete tenant.\n\nThis tenant has:\n⢠${counts.join('\n⢠')}\n\n${data.message || 'Please delete these resources first or use force delete.'}`
147 : data.message || data.error || 'Failed to delete tenant';
151 alert(data.message || data.error || 'Failed to delete tenant');
156 const data = await res.json();
157 alert(`ā Tenant "${tenantToDelete.name}" deleted successfully${data.cascade_deleted ? ' (including all related data)' : ''}`);
159 setShowDeleteModal(false);
160 setTenantToDelete(null);
161 setDeleteRelatedData(null);
162 setForceDelete(false);
166 alert(err.message || 'Failed to delete tenant');
172 function cancelDelete() {
173 setShowDeleteModal(false);
174 setTenantToDelete(null);
175 setDeleteRelatedData(null);
176 setForceDelete(false);
179 const totalPages = Math.ceil(total / limit);
183 <div className="page-content">
184 <div className="page-header">
185 <h2>Tenant Management</h2>
186 <button className="btn" onClick={() => navigate('/tenants/new')}>
187 <span className="material-symbols-outlined">add</span> Create Tenant
191 {error && <div className="error-message">{error}</div>}
194 <div className="filters-container">
195 <div className="filter-group">
198 placeholder="Search tenants..."
201 setSearch(e.target.value);
204 className="filter-input"
208 <div className="filter-group">
212 setStatusFilter(e.target.value);
215 className="filter-select"
217 <option value="">All Statuses</option>
218 <option value="active">Active</option>
219 <option value="suspended">Suspended</option>
220 <option value="inactive">Inactive</option>
224 <button className="btn" onClick={fetchTenants}>
225 <span className="material-symbols-outlined">refresh</span> Refresh
229 {/* Tenants Table */}
231 <div className="loading">Loading tenants...</div>
232 ) : tenants.length === 0 ? (
233 <div className="empty-state">
234 <span className="material-symbols-outlined">domain</span>
235 <p>No tenants found</p>
239 <div className="table-container">
240 <table className="data-table">
255 {tenants.map((tenant) => (
256 <tr key={tenant.tenant_id}>
258 <strong>{tenant.name}</strong>
261 <code>{tenant.subdomain}</code>
264 <span className={`status-badge status-${tenant.status?.toLowerCase()}`}>
273 backgroundColor: 'var(--primary)',
284 backgroundColor: 'var(--bg-secondary)',
285 color: 'var(--text-muted)',
292 <td>{tenant.user_count || 0}</td>
293 <td>{tenant.customer_count || 0}</td>
294 <td>{tenant.ticket_count || 0}</td>
295 <td>{new Date(tenant.created_at).toLocaleDateString()}</td>
297 <div style={{ display: 'flex', gap: '8px' }}>
299 className="btn btn-sm"
300 onClick={() => navigate(`/tenants/${tenant.tenant_id}`)}
303 <span className="material-symbols-outlined">visibility</span>
306 className="btn btn-sm"
307 onClick={() => navigate(`/tenants/${tenant.tenant_id}/edit`)}
310 <span className="material-symbols-outlined">edit</span>
313 className="btn btn-sm"
314 onClick={() => handleDelete(tenant.tenant_id, tenant.name)}
316 style={{ color: 'var(--danger)' }}
317 disabled={tenant.is_msp}
319 <span className="material-symbols-outlined">delete</span>
331 <div className="pagination">
334 onClick={() => setPage(p => Math.max(1, p - 1))}
335 disabled={page === 1}
339 <span className="pagination-info">
340 Page {page} of {totalPages} ({total} total)
344 onClick={() => setPage(p => Math.min(totalPages, p + 1))}
345 disabled={page === totalPages}
354 {/* Delete Confirmation Modal */}
355 {showDeleteModal && tenantToDelete && (
356 <div className="modal-overlay" onClick={cancelDelete}>
357 <div className="modal-content" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '600px' }}>
358 <div className="modal-header">
360 <span className="material-symbols-outlined" style={{ color: 'var(--danger)', verticalAlign: 'middle' }}>
365 <button className="modal-close" onClick={cancelDelete}>
366 <span className="material-symbols-outlined">close</span>
370 <div className="modal-body">
371 <div style={{ marginBottom: '20px' }}>
372 <p style={{ fontSize: '1.1em', marginBottom: '10px' }}>
373 Are you sure you want to delete tenant <strong>"{tenantToDelete.name}"</strong>?
375 <p style={{ color: 'var(--text-muted)', marginBottom: '20px' }}>
376 This action cannot be undone.
380 {deleteRelatedData && (
382 backgroundColor: 'var(--bg-secondary)',
387 <h3 style={{ marginBottom: '10px', fontSize: '1em' }}>
388 <span className="material-symbols-outlined" style={{ verticalAlign: 'middle', fontSize: '1.2em' }}>
391 {' '}Tenant Data Summary
394 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', fontSize: '0.95em' }}>
395 <div>š„ Users: <strong>{deleteRelatedData.user_count || 0}</strong></div>
396 <div>š¢ Customers: <strong>{deleteRelatedData.customer_count || 0}</strong></div>
397 <div>š« Tickets: <strong>{deleteRelatedData.ticket_count || 0}</strong></div>
398 <div>š Invoices: <strong>{deleteRelatedData.invoice_count || 0}</strong></div>
399 <div>š Contracts: <strong>{deleteRelatedData.contract_count || 0}</strong></div>
400 <div>š„ļø Agents: <strong>{deleteRelatedData.agent_count || 0}</strong></div>
403 {(parseInt(deleteRelatedData.user_count) > 0 ||
404 parseInt(deleteRelatedData.customer_count) > 0 ||
405 parseInt(deleteRelatedData.ticket_count) > 0 ||
406 parseInt(deleteRelatedData.invoice_count) > 0 ||
407 parseInt(deleteRelatedData.contract_count) > 0 ||
408 parseInt(deleteRelatedData.agent_count) > 0) && (
413 backgroundColor: 'var(--warning-bg, #fff3e0)',
414 border: '1px solid var(--warning, #ff9800)',
416 color: 'var(--warning-text, #e65100)'
418 <strong>ā ļø Warning:</strong> This tenant has data that will be affected.
423 alignItems: 'center',
426 backgroundColor: 'var(--danger-bg, #ffebee)',
427 border: '1px solid var(--danger, #f44336)',
433 checked={forceDelete}
434 onChange={(e) => setForceDelete(e.target.checked)}
435 style={{ marginRight: '10px' }}
437 <span style={{ fontSize: '0.95em' }}>
438 <strong>Force delete</strong> - Permanently delete all related data (users, customers, tickets, invoices, contracts, agents, domains, etc.)
447 <div className="modal-footer">
450 onClick={cancelDelete}
456 className="btn btn-danger"
457 onClick={confirmDelete}
458 disabled={deleting || (deleteRelatedData &&
459 (parseInt(deleteRelatedData.user_count) > 0 ||
460 parseInt(deleteRelatedData.customer_count) > 0 ||
461 parseInt(deleteRelatedData.ticket_count) > 0 ||
462 parseInt(deleteRelatedData.invoice_count) > 0 ||
463 parseInt(deleteRelatedData.contract_count) > 0 ||
464 parseInt(deleteRelatedData.agent_count) > 0) && !forceDelete)}
468 <span className="material-symbols-outlined spinning">progress_activity</span>
473 <span className="material-symbols-outlined">delete_forever</span>
474 {forceDelete ? 'Force Delete Tenant' : 'Delete Tenant'}
487export default Tenants;