1// This file has been removed as its logic has been merged into Contracts.jsx
2import { useState, useEffect } from 'react';
3// import removed: TenantSelector
4import { useNavigate } from 'react-router-dom';
5import MainLayout from '../components/Layout/MainLayout';
7import { apiFetch } from '../lib/api';
10 // Tenant selection logic
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);
21 const token = localStorage.getItem('token');
25 const payload = token.split('.')[1];
26 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
27 tenantId = decoded.tenant_id || decoded.tenantId || '';
30 // Use React state for filters only (no persistence)
31 const [filters, setFilters] = useState({
33 billing_interval: 'all',
40 localStorage.setItem('contractFilters', JSON.stringify(filters));
43 const fetchContracts = async () => {
46 const token = localStorage.getItem('token');
47 let url = '/contracts';
48 const params = new URLSearchParams();
49 params.set('page', page);
50 params.set('limit', perPage);
51 if (search) params.set('search', search);
52 if (filters.status && filters.status !== 'all') params.set('status', filters.status);
53 if (filters.billing_interval && filters.billing_interval !== 'all') params.set('billing_interval', filters.billing_interval);
54 if (filters.date_from) params.set('date_from', filters.date_from);
55 if (filters.date_to) params.set('date_to', filters.date_to);
56 const queryString = params.toString();
57 if (queryString) url += `?${queryString}`;
58 const res = await apiFetch(url, {
59 headers: { Authorization: `Bearer ${token}` }
61 if (!res.ok) throw new Error('Failed to load contracts');
62 const data = await res.json();
63 setContracts(data.contracts);
64 setTotalPages(Math.ceil(data.total / perPage));
67 setError(err.message);
75 }, [page, search, filters, tenantId]);
77 const handleFilterChange = (key, value) => {
78 setFilters(prev => ({ ...prev, [key]: value }));
82 const handleClearFilters = () => {
83 const defaultFilters = { status: 'all', billing_interval: 'all', date_from: '', date_to: '' };
84 setFilters(defaultFilters);
89 const hasActiveFilters = () => {
90 return filters.status !== 'all' || filters.billing_interval !== 'all' || filters.date_from || filters.date_to || search;
95 <div className="page-content">
96 <div className="page-header">
97 <div className="header-content">
99 <button className="btn primary" onClick={() => navigate('/contracts/new')}>
100 <span className="material-symbols-outlined">add</span>
106 {/* Search and Filter Bar */}
107 <div className="toolbar">
108 <div className="toolbar-left">
109 <div className="search-box">
110 <span className="material-symbols-outlined">search</span>
112 placeholder="Search contracts by title, customer..."
114 onChange={e => { setSearch(e.target.value); setPage(1); }}
118 <div className="toolbar-right">
120 className={`btn ${showFilters ? 'active' : ''}`}
121 onClick={() => setShowFilters(!showFilters)}
122 title="Toggle filters"
124 <span className="material-symbols-outlined">tune</span>
126 {hasActiveFilters() && (
127 <span className="badge">{Object.values(filters).filter(v => v && v !== 'all').length + (search ? 1 : 0)}</span>
130 {hasActiveFilters() && (
131 <button className="btn" onClick={handleClearFilters}>
132 <span className="material-symbols-outlined">clear_all</span>
141 <div className="filter-panel">
142 <h3>Filter Options</h3>
143 <div className="filter-grid">
144 <div className="filter-group">
145 <label>Status</label>
147 value={filters.status}
148 onChange={e => handleFilterChange('status', e.target.value)}
150 <option value="all">All Statuses</option>
151 <option value="active">Active</option>
152 <option value="inactive">Inactive</option>
153 <option value="expired">Expired</option>
154 <option value="cancelled">Cancelled</option>
158 <div className="filter-group">
159 <label>Billing Interval</label>
161 value={filters.billing_interval}
162 onChange={e => handleFilterChange('billing_interval', e.target.value)}
164 <option value="all">All Intervals</option>
165 <option value="monthly">Monthly</option>
166 <option value="yearly">Yearly</option>
167 <option value="custom">Custom</option>
171 <div className="filter-group">
172 <label>Start Date From</label>
175 value={filters.date_from}
176 onChange={e => handleFilterChange('date_from', e.target.value)}
180 <div className="filter-group">
181 <label>Start Date To</label>
184 value={filters.date_to}
185 onChange={e => handleFilterChange('date_to', e.target.value)}
190 {hasActiveFilters() && (
191 <div className="active-filters">
192 <div className="active-filters-header">
193 <span className="filter-label">Active Filters:</span>
195 <div className="filter-tags">
196 {filters.status !== 'all' && (
197 <span className="filter-tag">
198 Status: {filters.status}
199 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('status', 'all'); }}>×</button>
202 {filters.billing_interval !== 'all' && (
203 <span className="filter-tag">
204 Billing: {filters.billing_interval}
205 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('billing_interval', 'all'); }}>×</button>
208 {filters.date_from && (
209 <span className="filter-tag">
210 From: {filters.date_from}
211 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('date_from', ''); }}>×</button>
214 {filters.date_to && (
215 <span className="filter-tag">
216 To: {filters.date_to}
217 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('date_to', ''); }}>×</button>
221 <span className="filter-tag">
223 <button onClick={(e) => { e.stopPropagation(); setSearch(''); }}>×</button>
232 {error && <div className="error-message">{error}</div>}
235 <div className="loading">Loading contracts...</div>
238 <div className="table-container">
239 <table className="data-table">
246 <th>Billing Interval</th>
250 <th>Next Billing</th>
256 {contracts.map(contract => (
257 <tr key={contract.contract_id}>
258 <td>#{contract.contract_id}</td>
262 onClick={(e) => { e.preventDefault(); navigate(`/contracts/${contract.contract_id}`); }}
263 style={{ color: 'var(--primary)', textDecoration: 'none', cursor: 'pointer' }}
264 onMouseEnter={(e) => e.target.style.textDecoration = 'underline'}
265 onMouseLeave={(e) => e.target.style.textDecoration = 'none'}
270 <td className="col-tenant">{contract.tenant_name || 'Root'}</td>
271 <td className="col-customer">{contract.customer_name || 'N/A'}</td>
273 <span className="billing-badge">{contract.billing_interval || 'monthly'}</span>
276 <span className="usage-badge">
277 {contract.current_devices || 0}/{contract.max_devices || 0}
281 <span className="usage-badge">
282 {contract.current_contacts || 0}/{contract.max_contacts || 0}
286 <span className="usage-badge">
287 {contract.current_products || 0}/{contract.max_products || 0}
290 <td>{contract.next_billing_date ? new Date(contract.next_billing_date).toLocaleDateString() : 'N/A'}</td>
292 <span className={`status-badge ${contract.status?.toLowerCase() || 'active'}`}>
293 {contract.status || 'active'}
299 onClick={() => navigate(`/contracts/${contract.contract_id}/edit`)}
301 <span className="material-symbols-outlined">edit</span>
310 <div className="pagination">
311 <button className="btn" disabled={page === 1} onClick={() => setPage(p => Math.max(1, p - 1))}>
312 <span className="material-symbols-outlined">navigate_before</span>
315 Page {page} of {totalPages}
317 <button className="btn" disabled={page === totalPages} onClick={() => setPage(p => Math.min(totalPages, p + 1))}>
318 <span className="material-symbols-outlined">navigate_next</span>
328export default Contracts;