EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
Contracts.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2// import removed: TenantSelector
3import MainLayout from '../components/Layout/MainLayout';
4import { useNavigate } from 'react-router-dom';
5import './Tickets.css';
6import { apiFetch } from '../lib/api';
7import { isAdmin } from '../utils/auth';
8import { notifySuccess, notifyError } from '../utils/notifications';
9
10function Contracts() {
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');
21 let tenantId = '';
22 if (token) {
23 try {
24 const payload = token.split('.')[1];
25 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
26 tenantId = decoded.tenant_id || decoded.tenantId || '';
27 } catch { }
28 }
29 // Use React state for filters only (no persistence)
30 const [filters, setFilters] = useState({
31 status: 'all',
32 billing_interval: 'all',
33 date_from: '',
34 date_to: ''
35 });
36 const perPage = 10;
37 useEffect(() => {
38 localStorage.setItem('contractFilters', JSON.stringify(filters));
39 }, [filters]);
40 const fetchContracts = async () => {
41 setLoading(true);
42 try {
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}` }
57 });
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));
62 } catch (err) {
63 console.error(err);
64 setError(err.message);
65 } finally {
66 setLoading(false);
67 }
68 };
69 useEffect(() => {
70 fetchContracts();
71 }, [page, search, filters, tenantId]);
72 const handleFilterChange = (key, value) => {
73 setFilters(prev => ({ ...prev, [key]: value }));
74 setPage(1);
75 };
76 const handleClearFilters = () => {
77 const defaultFilters = { status: 'all', billing_interval: 'all', date_from: '', date_to: '' };
78 setFilters(defaultFilters);
79 setSearch('');
80 setPage(1);
81 };
82 const hasActiveFilters = () => {
83 return filters.status !== 'all' || filters.billing_interval !== 'all' || filters.date_from || filters.date_to || search;
84 };
85
86 const handleDeleteContract = async (contractId) => {
87 setLoading(true);
88 try {
89 const token = localStorage.getItem('token');
90 const res = await apiFetch(`/contracts/${contractId}`, {
91 method: 'DELETE',
92 headers: { Authorization: `Bearer ${token}` }
93 });
94 if (!res.ok) {
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);
99 }
100 setDeleteConfirm(null);
101 await notifySuccess('Contract Deleted', 'Contract has been deleted successfully');
102 fetchContracts();
103 } catch (err) {
104 setError(err.message);
105 } finally {
106 setLoading(false);
107 }
108 };
109
110 return (
111 <MainLayout>
112 <div className="page-content">
113 <div className="page-header">
114 <div className="header-content">
115 <h2>Contracts</h2>
116 <button className="btn primary" onClick={() => navigate('/contracts/new')}>
117 <span className="material-symbols-outlined">add</span>
118 Create Contract
119 </button>
120 </div>
121 </div>
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>
127 <input
128 placeholder="Search contracts by title, customer..."
129 value={search}
130 onChange={e => { setSearch(e.target.value); setPage(1); }}
131 />
132 </div>
133 </div>
134 <div className="toolbar-right">
135 <button
136 className={`btn ${showFilters ? 'active' : ''}`}
137 onClick={() => setShowFilters(!showFilters)}
138 title="Toggle filters"
139 >
140 <span className="material-symbols-outlined">tune</span>
141 Filters
142 {hasActiveFilters() && (
143 <span className="badge">{Object.values(filters).filter(v => v && v !== 'all').length + (search ? 1 : 0)}</span>
144 )}
145 </button>
146 {hasActiveFilters() && (
147 <button className="btn" onClick={handleClearFilters}>
148 <span className="material-symbols-outlined">clear_all</span>
149 Clear All
150 </button>
151 )}
152 </div>
153 </div>
154 {/* Filter Panel */}
155 {showFilters && (
156 <div className="filter-panel">
157 <h3>Filter Options</h3>
158 <div className="filter-grid">
159 <div className="filter-group">
160 <label>Status</label>
161 <select
162 value={filters.status}
163 onChange={e => handleFilterChange('status', e.target.value)}
164 >
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>
170 </select>
171 </div>
172 <div className="filter-group">
173 <label>Billing Interval</label>
174 <select
175 value={filters.billing_interval}
176 onChange={e => handleFilterChange('billing_interval', e.target.value)}
177 >
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>
182 </select>
183 </div>
184 <div className="filter-group">
185 <label>Start Date From</label>
186 <input
187 type="date"
188 value={filters.date_from}
189 onChange={e => handleFilterChange('date_from', e.target.value)}
190 />
191 </div>
192 <div className="filter-group">
193 <label>Start Date To</label>
194 <input
195 type="date"
196 value={filters.date_to}
197 onChange={e => handleFilterChange('date_to', e.target.value)}
198 />
199 </div>
200 </div>
201 {hasActiveFilters() && (
202 <div className="active-filters">
203 <div className="active-filters-header">
204 <span className="filter-label">Active Filters:</span>
205 </div>
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>
211 </span>
212 )}
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>
217 </span>
218 )}
219 {filters.date_from && (
220 <span className="filter-tag">
221 From: {filters.date_from}
222 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('date_from', ''); }}>×</button>
223 </span>
224 )}
225 {filters.date_to && (
226 <span className="filter-tag">
227 To: {filters.date_to}
228 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('date_to', ''); }}>×</button>
229 </span>
230 )}
231 {search && (
232 <span className="filter-tag">
233 Search: "{search}"
234 <button onClick={(e) => { e.stopPropagation(); setSearch(''); }}>×</button>
235 </span>
236 )}
237 </div>
238 </div>
239 )}
240 </div>
241 )}
242 {error && <div className="error-message">{error}</div>}
243 {loading ? (
244 <div className="loading">Loading contracts...</div>
245 ) : (
246 <>
247 <div className="table-container">
248 <table className="data-table">
249 <thead>
250 <tr>
251 <th>ID</th>
252 <th>Title</th>
253 <th>Tenant</th>
254 <th>Customer</th>
255 <th>Billing Interval</th>
256 <th>Devices</th>
257 <th>Contacts</th>
258 <th>Products</th>
259 <th>Next Billing</th>
260 <th>Status</th>
261 <th>Actions</th>
262 </tr>
263 </thead>
264 <tbody>
265 {contracts.map(contract => (
266 <tr key={contract.contract_id}>
267 <td>#{contract.contract_id}</td>
268 <td>
269 <a
270 href="#"
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'}
275 >
276 {contract.title}
277 </a>
278 </td>
279 <td className="col-tenant">{contract.tenant_name || 'Root'}</td>
280 <td className="col-customer">{contract.customer_name || 'N/A'}</td>
281 <td>
282 <span className="billing-badge">{contract.billing_interval || 'monthly'}</span>
283 </td>
284 <td>
285 <span className="usage-badge">
286 {contract.current_devices || 0}/{contract.max_devices || 0}
287 </span>
288 </td>
289 <td>
290 <span className="usage-badge">
291 {contract.current_contacts || 0}/{contract.max_contacts || 0}
292 </span>
293 </td>
294 <td>
295 <span className="usage-badge">
296 {contract.current_products || 0}/{contract.max_products || 0}
297 </span>
298 </td>
299 <td>{contract.next_billing_date ? new Date(contract.next_billing_date).toLocaleDateString() : 'N/A'}</td>
300 <td>
301 <span className={`status-badge ${contract.status?.toLowerCase() || 'active'}`}>
302 {contract.status || 'active'}
303 </span>
304 </td>
305 <td>
306 <button
307 className="btn"
308 onClick={() => navigate(`/contracts/${contract.contract_id}/edit`)}
309 title="Edit contract"
310 >
311 <span className="material-symbols-outlined">edit</span>
312 </button>
313 {isAdmin() && (
314 <button
315 className="btn danger"
316 onClick={() => setDeleteConfirm(contract)}
317 title="Delete contract"
318 style={{ marginLeft: '4px' }}
319 >
320 <span className="material-symbols-outlined">delete</span>
321 </button>
322 )}
323 </td>
324 </tr>
325 ))}
326 </tbody>
327 </table>
328 </div>
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>
332 </button>
333
334 {/* Delete Confirmation Modal */}
335 {deleteConfirm && (
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>
340 <button
341 className="btn"
342 onClick={() => setDeleteConfirm(null)}
343 style={{ padding: '4px', minWidth: 'auto' }}
344 >
345 <span className="material-symbols-outlined">close</span>
346 </button>
347 </div>
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'.
353 </p>
354 </div>
355 <div className="modal-footer">
356 <button
357 className="btn"
358 onClick={() => setDeleteConfirm(null)}
359 style={{ marginRight: '8px' }}
360 >
361 Cancel
362 </button>
363 <button
364 className="btn danger"
365 onClick={() => handleDeleteContract(deleteConfirm.contract_id)}
366 >
367 Delete
368 </button>
369 </div>
370 </div>
371 </div>
372 )}
373 <span>
374 Page {page} of {totalPages}
375 </span>
376 <button className="btn" disabled={page === totalPages} onClick={() => setPage(p => Math.min(totalPages, p + 1))}>
377 <span className="material-symbols-outlined">navigate_next</span>
378 </button>
379 </div>
380 </>
381 )}
382 </div>
383 </MainLayout>
384 );
385}
386
387
388
389export default Contracts;