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';
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');
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 || '';
33 }, [page, search, tenantId]);
35 // Removed selectedTenantId effect
37 // Detect MSP and load tenants for mapping, also check if root admin
41 const res = await apiFetch('/tenants', { method: 'GET', credentials: 'include' });
44 const data = await res.json();
46 (data.tenants || data || []).forEach(t => { if (t.tenant_id) map[t.tenant_id] = t.name; });
49 // Check if user is root admin (subdomain = 'admin' or is_msp = true)
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);
60 setIsRootAdmin(false);
64 setIsRootAdmin(false);
69 const fetchProducts = async () => {
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);
82 const text = await res.text();
83 console.error('[Products] Fetch failed:', text);
84 throw new Error(text || 'Fetch failed');
86 const data = await res.json();
87 setProducts(data.products || []);
88 setTotal(data.total || 0);
91 console.error('[Products] Error:', err);
92 setError('Failed to load products: ' + (err.message || err));
98 const handleDelete = async (productId, productName) => {
99 if (!confirm(`Are you sure you want to delete "${productName}"? This action cannot be undone.`)) {
104 const res = await apiFetch(`/products/${productId}`, {
106 credentials: 'include'
109 if (res.status === 204) {
110 // Success - refresh the products list
113 // Try to parse JSON error message
115 const errorData = await res.json();
116 setError(errorData.message || errorData.error || 'Failed to delete product');
118 const errorText = await res.text();
119 setError(errorText || 'Failed to delete product');
123 console.error('[Products] Delete error:', err);
124 setError('Failed to delete product: ' + (err.message || err));
130 <div className="page-content">
131 <div className="page-header">
132 <div className="header-content">
134 <div className="header-actions">
135 <div className="search-box">
136 <span className="material-symbols-outlined">search</span>
138 placeholder="Search by name, supplier, or product codes"
140 onChange={e => { setSearch(e.target.value); setPage(1); }}
143 <button className="btn primary" onClick={() => navigate('/products/new')}>
144 <span className="material-symbols-outlined">add</span>
151 {error && <div className="error-message">{error}</div>}
152 {loading ? <div className="loading">Loading...</div> : (
154 <table className="data-table">
158 {isMSP && <th>Tenant</th>}
160 <th>Product Codes</th>
169 <tr key={p.product_id}>
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}` : '-'))}
176 <td>{p.supplier}</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>}
183 ${Number(p.price_retail || p.price_ex_tax || 0).toFixed(2)}
185 <td>{p.is_stock ? `${p.stock_quantity}` : '-'}</td>
187 <span className={`status-badge status-${p.is_service ? 'pending' : 'active'}`}>
188 {p.is_service ? 'Service' : 'Product'}
194 onClick={() => navigate(`/products/${p.product_id}`)}
196 <span className="material-symbols-outlined">edit</span>
200 className="btn btn-danger"
201 onClick={() => handleDelete(p.product_id, p.name)}
202 style={{ marginLeft: '8px' }}
203 title="Delete product"
205 <span className="material-symbols-outlined">delete</span>
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>
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>