1import { useState, useEffect } from 'react';
2// import removed: TenantSelector
3import MainLayout from '../components/Layout/MainLayout';
4import { useNavigate } from 'react-router-dom';
6import { apiFetch } from '../lib/api';
7import { isAdmin } from '../utils/auth';
8import { notifySuccess, notifyError } from '../utils/notifications';
11 const navigate = useNavigate();
12 const [contracts, setContracts] = useState([]);
13 const [loading, setLoading] = useState(false);
14 const [error, setError] = useState('');
15 const [page, setPage] = useState(1);
16 const [totalPages, setTotalPages] = useState(1);
17 const [search, setSearch] = useState('');
18 const [showFilters, setShowFilters] = useState(false);
19 const [deleteConfirm, setDeleteConfirm] = useState(null);
20 const token = localStorage.getItem('token');
24 const payload = token.split('.')[1];
25 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
26 tenantId = decoded.tenant_id || decoded.tenantId || '';
29 // Use React state for filters only (no persistence)
30 const [filters, setFilters] = useState({
32 billing_interval: 'all',
38 localStorage.setItem('contractFilters', JSON.stringify(filters));
40 const fetchContracts = async () => {
43 const token = localStorage.getItem('token');
44 let url = '/contracts';
45 const params = new URLSearchParams();
46 params.set('page', page);
47 params.set('limit', perPage);
48 if (search) params.set('search', search);
49 if (filters.status && filters.status !== 'all') params.set('status', filters.status);
50 if (filters.billing_interval && filters.billing_interval !== 'all') params.set('billing_interval', filters.billing_interval);
51 if (filters.date_from) params.set('date_from', filters.date_from);
52 if (filters.date_to) params.set('date_to', filters.date_to);
53 const queryString = params.toString();
54 if (queryString) url += `?${queryString}`;
55 const res = await apiFetch(url, {
56 headers: { Authorization: `Bearer ${token}` }
58 if (!res.ok) throw new Error('Failed to load contracts');
59 const data = await res.json();
60 setContracts(data.contracts);
61 setTotalPages(Math.ceil(data.total / perPage));
64 setError(err.message);
71 }, [page, search, filters, tenantId]);
72 const handleFilterChange = (key, value) => {
73 setFilters(prev => ({ ...prev, [key]: value }));
76 const handleClearFilters = () => {
77 const defaultFilters = { status: 'all', billing_interval: 'all', date_from: '', date_to: '' };
78 setFilters(defaultFilters);
82 const hasActiveFilters = () => {
83 return filters.status !== 'all' || filters.billing_interval !== 'all' || filters.date_from || filters.date_to || search;
86 const handleDeleteContract = async (contractId) => {
89 const token = localStorage.getItem('token');
90 const res = await apiFetch(`/contracts/${contractId}`, {
92 headers: { Authorization: `Bearer ${token}` }
95 const errorData = await res.json();
96 const errorMsg = errorData.error || 'Failed to delete contract';
97 await notifyError('Delete Failed', errorMsg);
98 throw new Error(errorMsg);
100 setDeleteConfirm(null);
101 await notifySuccess('Contract Deleted', 'Contract has been deleted successfully');
104 setError(err.message);
112 <div className="page-content">
113 <div className="page-header">
114 <div className="header-content">
116 <button className="btn primary" onClick={() => navigate('/contracts/new')}>
117 <span className="material-symbols-outlined">add</span>
122 {/* Search and Filter Bar */}
123 <div className="toolbar">
124 <div className="toolbar-left">
125 <div className="search-box">
126 <span className="material-symbols-outlined">search</span>
128 placeholder="Search contracts by title, customer..."
130 onChange={e => { setSearch(e.target.value); setPage(1); }}
134 <div className="toolbar-right">
136 className={`btn ${showFilters ? 'active' : ''}`}
137 onClick={() => setShowFilters(!showFilters)}
138 title="Toggle filters"
140 <span className="material-symbols-outlined">tune</span>
142 {hasActiveFilters() && (
143 <span className="badge">{Object.values(filters).filter(v => v && v !== 'all').length + (search ? 1 : 0)}</span>
146 {hasActiveFilters() && (
147 <button className="btn" onClick={handleClearFilters}>
148 <span className="material-symbols-outlined">clear_all</span>
156 <div className="filter-panel">
157 <h3>Filter Options</h3>
158 <div className="filter-grid">
159 <div className="filter-group">
160 <label>Status</label>
162 value={filters.status}
163 onChange={e => handleFilterChange('status', e.target.value)}
165 <option value="all">All Statuses</option>
166 <option value="active">Active</option>
167 <option value="inactive">Inactive</option>
168 <option value="expired">Expired</option>
169 <option value="cancelled">Cancelled</option>
172 <div className="filter-group">
173 <label>Billing Interval</label>
175 value={filters.billing_interval}
176 onChange={e => handleFilterChange('billing_interval', e.target.value)}
178 <option value="all">All Intervals</option>
179 <option value="monthly">Monthly</option>
180 <option value="yearly">Yearly</option>
181 <option value="custom">Custom</option>
184 <div className="filter-group">
185 <label>Start Date From</label>
188 value={filters.date_from}
189 onChange={e => handleFilterChange('date_from', e.target.value)}
192 <div className="filter-group">
193 <label>Start Date To</label>
196 value={filters.date_to}
197 onChange={e => handleFilterChange('date_to', e.target.value)}
201 {hasActiveFilters() && (
202 <div className="active-filters">
203 <div className="active-filters-header">
204 <span className="filter-label">Active Filters:</span>
206 <div className="filter-tags">
207 {filters.status !== 'all' && (
208 <span className="filter-tag">
209 Status: {filters.status}
210 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('status', 'all'); }}>×</button>
213 {filters.billing_interval !== 'all' && (
214 <span className="filter-tag">
215 Billing: {filters.billing_interval}
216 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('billing_interval', 'all'); }}>×</button>
219 {filters.date_from && (
220 <span className="filter-tag">
221 From: {filters.date_from}
222 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('date_from', ''); }}>×</button>
225 {filters.date_to && (
226 <span className="filter-tag">
227 To: {filters.date_to}
228 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('date_to', ''); }}>×</button>
232 <span className="filter-tag">
234 <button onClick={(e) => { e.stopPropagation(); setSearch(''); }}>×</button>
242 {error && <div className="error-message">{error}</div>}
244 <div className="loading">Loading contracts...</div>
247 <div className="table-container">
248 <table className="data-table">
255 <th>Billing Interval</th>
259 <th>Next Billing</th>
265 {contracts.map(contract => (
266 <tr key={contract.contract_id}>
267 <td>#{contract.contract_id}</td>
271 onClick={(e) => { e.preventDefault(); navigate(`/contracts/${contract.contract_id}`); }}
272 style={{ color: 'var(--primary)', textDecoration: 'none', cursor: 'pointer' }}
273 onMouseEnter={(e) => e.target.style.textDecoration = 'underline'}
274 onMouseLeave={(e) => e.target.style.textDecoration = 'none'}
279 <td className="col-tenant">{contract.tenant_name || 'Root'}</td>
280 <td className="col-customer">{contract.customer_name || 'N/A'}</td>
282 <span className="billing-badge">{contract.billing_interval || 'monthly'}</span>
285 <span className="usage-badge">
286 {contract.current_devices || 0}/{contract.max_devices || 0}
290 <span className="usage-badge">
291 {contract.current_contacts || 0}/{contract.max_contacts || 0}
295 <span className="usage-badge">
296 {contract.current_products || 0}/{contract.max_products || 0}
299 <td>{contract.next_billing_date ? new Date(contract.next_billing_date).toLocaleDateString() : 'N/A'}</td>
301 <span className={`status-badge ${contract.status?.toLowerCase() || 'active'}`}>
302 {contract.status || 'active'}
308 onClick={() => navigate(`/contracts/${contract.contract_id}/edit`)}
309 title="Edit contract"
311 <span className="material-symbols-outlined">edit</span>
315 className="btn danger"
316 onClick={() => setDeleteConfirm(contract)}
317 title="Delete contract"
318 style={{ marginLeft: '4px' }}
320 <span className="material-symbols-outlined">delete</span>
329 <div className="pagination">
330 <button className="btn" disabled={page === 1} onClick={() => setPage(p => Math.max(1, p - 1))}>
331 <span className="material-symbols-outlined">navigate_before</span>
334 {/* Delete Confirmation Modal */}
336 <div className="modal-overlay" onClick={() => setDeleteConfirm(null)}>
337 <div className="modal-content" onClick={e => e.stopPropagation()}>
338 <div className="modal-header">
339 <h3>Confirm Delete</h3>
342 onClick={() => setDeleteConfirm(null)}
343 style={{ padding: '4px', minWidth: 'auto' }}
345 <span className="material-symbols-outlined">close</span>
348 <div className="modal-body">
349 <p>Are you sure you want to delete this contract?</p>
350 <p><strong>"{deleteConfirm.title}"</strong></p>
351 <p style={{ color: 'var(--warning)', fontSize: '0.9em' }}>
352 This will set the contract status to 'deleted'. This action can be reversed by changing the status back to 'active'.
355 <div className="modal-footer">
358 onClick={() => setDeleteConfirm(null)}
359 style={{ marginRight: '8px' }}
364 className="btn danger"
365 onClick={() => handleDeleteContract(deleteConfirm.contract_id)}
374 Page {page} of {totalPages}
376 <button className="btn" disabled={page === totalPages} onClick={() => setPage(p => Math.min(totalPages, p + 1))}>
377 <span className="material-symbols-outlined">navigate_next</span>
389export default Contracts;