EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
Products.jsx
Go to the documentation of this file.
1// import removed: TenantSelector
2import { useState, useEffect } from 'react';
3import MainLayout from '../components/Layout/MainLayout';
4import { useNavigate } from 'react-router-dom';
5import { apiFetch } from '../lib/api';
6
7export default function Products() {
8 const navigate = useNavigate();
9 const [products, setProducts] = useState([]);
10 const [total, setTotal] = useState(0);
11 const [page, setPage] = useState(1);
12 const [limit] = useState(25);
13 const [loading, setLoading] = useState(true);
14 const [error, setError] = useState(null);
15 const [search, setSearch] = useState('');
16 const [isMSP, setIsMSP] = useState(false);
17 const [isRootAdmin, setIsRootAdmin] = useState(false);
18 const [tenantsMap, setTenantsMap] = useState({});
19 const token = localStorage.getItem('token');
20 let tenantId = '';
21 let subdomain = '';
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 subdomain = decoded.subdomain || '';
28 } catch { }
29 }
30
31 useEffect(() => {
32 fetchProducts();
33 }, [page, search, tenantId]);
34
35 // Removed selectedTenantId effect
36
37 // Detect MSP and load tenants for mapping, also check if root admin
38 useEffect(() => {
39 (async () => {
40 try {
41 const res = await apiFetch('/tenants', { method: 'GET', credentials: 'include' });
42 if (res.ok) {
43 setIsMSP(true);
44 const data = await res.json();
45 const map = {};
46 (data.tenants || data || []).forEach(t => { if (t.tenant_id) map[t.tenant_id] = t.name; });
47 setTenantsMap(map);
48
49 // Check if user is root admin (subdomain = 'admin' or is_msp = true)
50 if (token) {
51 try {
52 const payload = token.split('.')[1];
53 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
54 const isRoot = decoded.subdomain === 'admin' || decoded.is_msp === true;
55 setIsRootAdmin(isRoot);
56 } catch { }
57 }
58 } else {
59 setIsMSP(false);
60 setIsRootAdmin(false);
61 }
62 } catch (e) {
63 setIsMSP(false);
64 setIsRootAdmin(false);
65 }
66 })();
67 }, []);
68
69 const fetchProducts = async () => {
70 setLoading(true);
71 try {
72 const params = new URLSearchParams();
73 params.set('page', page);
74 params.set('limit', limit);
75 if (search) params.set('search', search);
76 const queryString = params.toString();
77 const endpoint = queryString ? `/products?${queryString}` : '/products';
78 console.log('[Products] Fetching:', endpoint);
79 const res = await apiFetch(endpoint, { method: 'GET', credentials: 'include' });
80 console.log('[Products] Response status:', res.status);
81 if (!res.ok) {
82 const text = await res.text();
83 console.error('[Products] Fetch failed:', text);
84 throw new Error(text || 'Fetch failed');
85 }
86 const data = await res.json();
87 setProducts(data.products || []);
88 setTotal(data.total || 0);
89 setError(null);
90 } catch (err) {
91 console.error('[Products] Error:', err);
92 setError('Failed to load products: ' + (err.message || err));
93 } finally {
94 setLoading(false);
95 }
96 };
97
98 const handleDelete = async (productId, productName) => {
99 if (!confirm(`Are you sure you want to delete "${productName}"? This action cannot be undone.`)) {
100 return;
101 }
102
103 try {
104 const res = await apiFetch(`/products/${productId}`, {
105 method: 'DELETE',
106 credentials: 'include'
107 });
108
109 if (res.status === 204) {
110 // Success - refresh the products list
111 fetchProducts();
112 } else {
113 // Try to parse JSON error message
114 try {
115 const errorData = await res.json();
116 setError(errorData.message || errorData.error || 'Failed to delete product');
117 } catch {
118 const errorText = await res.text();
119 setError(errorText || 'Failed to delete product');
120 }
121 }
122 } catch (err) {
123 console.error('[Products] Delete error:', err);
124 setError('Failed to delete product: ' + (err.message || err));
125 }
126 };
127
128 return (
129 <MainLayout>
130 <div className="page-content">
131 <div className="page-header">
132 <div className="header-content">
133 <h2>Products</h2>
134 <div className="header-actions">
135 <div className="search-box">
136 <span className="material-symbols-outlined">search</span>
137 <input
138 placeholder="Search by name, supplier, or product codes"
139 value={search}
140 onChange={e => { setSearch(e.target.value); setPage(1); }}
141 />
142 </div>
143 <button className="btn primary" onClick={() => navigate('/products/new')}>
144 <span className="material-symbols-outlined">add</span>
145 New Product
146 </button>
147 </div>
148 </div>
149 </div>
150
151 {error && <div className="error-message">{error}</div>}
152 {loading ? <div className="loading">Loading...</div> : (
153 <div>
154 <table className="data-table">
155 <thead>
156 <tr>
157 <th>Name</th>
158 {isMSP && <th>Tenant</th>}
159 <th>Supplier</th>
160 <th>Product Codes</th>
161 <th>Price</th>
162 <th>Stock</th>
163 <th>Type</th>
164 <th>Actions</th>
165 </tr>
166 </thead>
167 <tbody>
168 {products.map(p => (
169 <tr key={p.product_id}>
170 <td>{p.name}</td>
171 {isMSP && (
172 <td title={p.tenant_id ? `Tenant #${p.tenant_id}` : ''}>
173 {p.tenant_id === 1 ? 'Root Tenant' : (p.tenant_name || tenantsMap[p.tenant_id] || (p.tenant_id ? `#${p.tenant_id}` : '-'))}
174 </td>
175 )}
176 <td>{p.supplier}</td>
177 <td>
178 {p.upc && <div>UPC: {p.upc}</div>}
179 {p.ean && <div>EAN: {p.ean}</div>}
180 {p.supplier_code && <div>Supplier: {p.supplier_code}</div>}
181 </td>
182 <td>
183 ${Number(p.price_retail || p.price_ex_tax || 0).toFixed(2)}
184 </td>
185 <td>{p.is_stock ? `${p.stock_quantity}` : '-'}</td>
186 <td>
187 <span className={`status-badge status-${p.is_service ? 'pending' : 'active'}`}>
188 {p.is_service ? 'Service' : 'Product'}
189 </span>
190 </td>
191 <td>
192 <button
193 className="btn"
194 onClick={() => navigate(`/products/${p.product_id}`)}
195 >
196 <span className="material-symbols-outlined">edit</span>
197 </button>
198 {isRootAdmin && (
199 <button
200 className="btn btn-danger"
201 onClick={() => handleDelete(p.product_id, p.name)}
202 style={{ marginLeft: '8px' }}
203 title="Delete product"
204 >
205 <span className="material-symbols-outlined">delete</span>
206 </button>
207 )}
208 </td>
209 </tr>
210 ))}
211 </tbody>
212 </table>
213
214 <div className="pagination">
215 <button className="btn" disabled={page <= 1} onClick={() => setPage(p => Math.max(1, p - 1))}>
216 <span className="material-symbols-outlined">navigate_before</span>
217 </button>
218 <span>Page {page} of {Math.ceil(total / limit)}</span>
219 <button className="btn" disabled={products.length < limit} onClick={() => setPage(p => p + 1)}>
220 <span className="material-symbols-outlined">navigate_next</span>
221 </button>
222 </div>
223 </div>
224 )}
225 </div>
226 </MainLayout>
227 );
228}