1import { apiFetch } from '../lib/api';
2import { useState, useEffect } from 'react';
3import { useNavigate } from 'react-router-dom';
4import MainLayout from '../components/Layout/MainLayout';
5// import removed: TenantSelector
6import '../styles/data-table.css';
8import './Customers.css';
9import DownloadAgentModal from "../components/DownloadAgentModal";
10import CustomerMergeModal from "../components/CustomerMergeModal";
11import { isAdmin } from '../utils/auth';
12import { notifySuccess, notifyError } from '../utils/notifications';
15 // selectedTenantId should come from props/context, not localStorage
16 // Remove all localStorage usage for tenant context
17 // If needed, get tenantId from JWT in MainLayout and pass as prop
18 const token = localStorage.getItem('token');
22 const payload = token.split('.')[1];
23 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
24 tenantId = decoded.tenant_id || decoded.tenantId || '';
27 const [showDownloadModal, setShowDownloadModal] = useState(false);
28 const [publicUrl, setPublicUrl] = useState('');
29 const navigate = useNavigate();
30 const [customers, setCustomers] = useState([]);
31 const [loading, setLoading] = useState(false);
32 const [error, setError] = useState('');
33 const [isMSP, setIsMSP] = useState(false);
34 const [tenantsMap, setTenantsMap] = useState({});
35 const [page, setPage] = useState(1);
36 const [totalPages, setTotalPages] = useState(1);
37 const [search, setSearch] = useState('');
38 const [showFilters, setShowFilters] = useState(false);
39 const [showDownload, setShowDownload] = useState(false);
40 const [selectedCustomer, setSelectedCustomer] = useState(null);
41 const [deleteConfirm, setDeleteConfirm] = useState(null);
42 const [showMergeModal, setShowMergeModal] = useState(false);
44 // Remove localStorage for filters, just use defaults
45 const [filters, setFilters] = useState({
52 // ...no localStorage for filters...
54 const fetchCustomers = async (fetchTenantId = tenantId) => {
57 let url = '/customers';
58 const params = new URLSearchParams();
59 params.set('page', page);
60 params.set('limit', perPage);
61 if (search) params.set('search', search);
62 if (filters.status && filters.status !== 'all') params.set('status', filters.status);
63 if (filters.date_from) params.set('date_from', filters.date_from);
64 if (filters.date_to) params.set('date_to', filters.date_to);
65 const queryString = params.toString();
66 if (queryString) url += `?${queryString}`;
68 const headers = { 'Content-Type': 'application/json' };
69 const res = await apiFetch(url, { method: 'GET', headers });
70 if (!res.ok) throw new Error('Failed to load customers');
72 const data = await res.json();
73 setCustomers(data.customers);
74 setTotalPages(Math.ceil(data.total / perPage));
77 setError(err.message);
83 fetchCustomers(tenantId);
84 }, [page, search, filters, tenantId]);
86 // Detect MSP and load tenants list to map tenant_id -> name
90 const res = await apiFetch('/tenants');
93 const data = await res.json();
95 (data.tenants || data || []).forEach(t => { if (t.tenant_id) map[t.tenant_id] = t.name; });
102 console.error('[Customers] tenants API error:', e);
107 const handleCreateClick = () => {
108 navigate('/customers/new');
111 const handleViewClick = (customerId) => {
112 navigate(`/customers/${customerId}`);
115 const handleEditClick = (customerId) => {
116 navigate(`/customers/${customerId}/edit`);
119 const handleFilterChange = (key, value) => {
120 setFilters(prev => ({ ...prev, [key]: value }));
124 const handleClearFilters = () => {
125 const defaultFilters = { status: 'all', date_from: '', date_to: '' };
126 setFilters(defaultFilters);
131 const handleDeleteCustomer = async (customerId) => {
134 const token = localStorage.getItem('token');
135 const res = await apiFetch(`/customers/${customerId}`, {
137 headers: { Authorization: `Bearer ${token}` }
140 const errorData = await res.json();
141 const errorMsg = errorData.error || 'Failed to delete customer';
142 await notifyError('Delete Failed', errorMsg);
143 throw new Error(errorMsg);
145 setDeleteConfirm(null);
146 await notifySuccess('Customer Deleted', 'Customer has been deleted successfully');
147 fetchCustomers(tenantId);
149 setError(err.message);
155 const handleDeleteClick = (customer) => {
156 setDeleteConfirm(customer);
159 const hasActiveFilters = () => {
160 return filters.status !== 'all' || filters.date_from || filters.date_to || search;
168 <div className="page-content">
169 <div className="page-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
170 <h2 style={{ margin: 0 }}>Customers</h2>
171 <div style={{ display: 'flex', gap: 8 }}>
172 <button className="btn primary" onClick={handleCreateClick}>
173 <span className="material-symbols-outlined">add</span>
177 <button className="btn" onClick={() => setShowMergeModal(true)}>
178 <span className="material-symbols-outlined">merge</span>
183 className="btn primary"
185 setSelectedCustomer(null);
186 setShowDownload(true);
188 title="Download Agent Installer"
189 style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
191 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
198 {/* ...existing code... */}
200 {/* Search and Filter Bar */}
201 <div className="toolbar">
202 <div className="toolbar-left">
203 <div className="search-box">
204 <span className="material-symbols-outlined">search</span>
206 placeholder="Search customers by name, email, phone, city..."
208 onChange={e => { setSearch(e.target.value); setPage(1); }}
212 <div className="toolbar-right">
214 className={`btn ${showFilters ? 'active' : ''}`}
215 onClick={() => setShowFilters(!showFilters)}
216 title="Toggle filters"
218 <span className="material-symbols-outlined">tune</span>
220 {hasActiveFilters() && (
221 <span className="badge">{Object.values(filters).filter(v => v && v !== 'all').length + (search ? 1 : 0)}</span>
224 {hasActiveFilters() && (
225 <button className="btn" onClick={handleClearFilters}>
226 <span className="material-symbols-outlined">clear_all</span>
235 <div className="filter-panel">
236 <h3>Filter Options</h3>
237 <div className="filter-grid">
238 <div className="filter-group">
239 <label>Status</label>
241 value={filters.status}
242 onChange={e => handleFilterChange('status', e.target.value)}
244 <option value="all">All Statuses</option>
245 <option value="active">Active</option>
246 <option value="inactive">Inactive</option>
250 <div className="filter-group">
251 <label>Date From</label>
254 value={filters.date_from}
255 onChange={e => handleFilterChange('date_from', e.target.value)}
259 <div className="filter-group">
260 <label>Date To</label>
263 value={filters.date_to}
264 onChange={e => handleFilterChange('date_to', e.target.value)}
269 {hasActiveFilters() && (
270 <div className="active-filters">
271 <div className="active-filters-header">
272 <span className="filter-label">Active Filters:</span>
274 <div className="filter-tags">
275 {filters.status !== 'all' && (
276 <span className="filter-tag">
277 Status: {filters.status}
278 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('status', 'all'); }}>×</button>
281 {filters.date_from && (
282 <span className="filter-tag">
283 From: {filters.date_from}
284 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('date_from', ''); }}>×</button>
287 {filters.date_to && (
288 <span className="filter-tag">
289 To: {filters.date_to}
290 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('date_to', ''); }}>×</button>
294 <span className="filter-tag">
296 <button onClick={(e) => { e.stopPropagation(); setSearch(''); }}>×</button>
305 {error && <div className="error-message">{error}</div>}
308 <div className="loading">Loading customers...</div>
311 <div className="table-container">
312 <table className="data-table">
316 {isMSP && <th>Tenant</th>}
317 <th>Company Name</th>
319 <th>Contact Name</th>
325 {customers.map(customer => (
326 <tr key={customer.customer_id}>
330 onClick={e => { e.preventDefault(); handleViewClick(customer.customer_id); }}
331 style={{ color: 'var(--primary)', textDecoration: 'none', cursor: 'pointer', fontWeight: 500 }}
332 onMouseEnter={e => e.target.style.textDecoration = 'underline'}
333 onMouseLeave={e => e.target.style.textDecoration = 'none'}
337 <span style={{ marginLeft: 6, color: '#888', fontSize: '0.95em' }}>#{customer.customer_id}</span>
340 <td title={customer.tenant_id ? `Tenant #${customer.tenant_id}` : ''}>
341 {customer.tenant_id === 1 ? 'Root Tenant' : (customer.tenant_name || tenantsMap[customer.tenant_id] || (customer.tenant_id ? `#${customer.tenant_id}` : '-'))}
347 onClick={(e) => { e.preventDefault(); handleViewClick(customer.customer_id); }}
348 style={{ color: 'var(--primary)', textDecoration: 'none', cursor: 'pointer' }}
349 onMouseEnter={(e) => e.target.style.textDecoration = 'underline'}
350 onMouseLeave={(e) => e.target.style.textDecoration = 'none'}
355 <td>{customer.email}</td>
356 <td>{customer.contact_name}</td>
358 <span className={`status-badge ${customer.status}`}>
365 onClick={() => handleEditClick(customer.customer_id)}
366 style={{ marginRight: '8px' }}
368 <span className="material-symbols-outlined">edit</span>
371 className="btn btn-sm primary"
373 setSelectedCustomer(customer);
374 setShowDownload(true);
376 title="Download Agent Installer"
377 style={{ display: 'inline-flex', alignItems: 'center', gap: 4, marginRight: '8px' }}
379 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
386 className="btn danger"
387 onClick={() => handleDeleteClick(customer)}
388 title="Delete Customer"
390 <span className="material-symbols-outlined">delete</span>
401 <div className="pagination">
402 <button className="btn" disabled={page === 1} onClick={() => setPage(p => Math.max(1, p - 1))}>
403 <span className="material-symbols-outlined">navigate_before</span>
406 Page {page} of {totalPages}
408 <button className="btn" disabled={page === totalPages} onClick={() => setPage(p => Math.min(totalPages, p + 1))}>
409 <span className="material-symbols-outlined">navigate_next</span>
415 {showDownload && selectedCustomer && (
417 onClose={() => setShowDownload(false)}
418 customerId={selectedCustomer.customer_id}
419 customerName={selectedCustomer.name}
424 <div className="modal-overlay" onClick={() => setDeleteConfirm(null)}>
425 <div className="modal-content" onClick={e => e.stopPropagation()}>
426 <div className="modal-header">
427 <h3>Confirm Delete</h3>
430 onClick={() => setDeleteConfirm(null)}
431 style={{ padding: '4px', minWidth: 'auto' }}
433 <span className="material-symbols-outlined">close</span>
436 <div className="modal-body">
437 <p>Are you sure you want to delete the customer:</p>
438 <p><strong>"{deleteConfirm.name}"</strong></p>
439 <p style={{ color: 'var(--error)', fontSize: '0.9em' }}>
440 This action cannot be undone. This will fail if the customer has existing tickets or invoices.
443 <div className="modal-footer">
446 onClick={() => setDeleteConfirm(null)}
447 style={{ marginRight: '8px' }}
452 className="btn danger"
453 onClick={() => handleDeleteCustomer(deleteConfirm.customer_id)}
462 {/* Customer Merge Modal */}
464 isOpen={showMergeModal}
465 onClose={() => setShowMergeModal(false)}
466 onMergeComplete={() => fetchCustomers(tenantId)}
467 customers={customers}
475export default Customers;