EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
PurchaseOrders.jsx
Go to the documentation of this file.
1import { useEffect, useState } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { apiFetch } from '../lib/api';
4
5import MainLayout from '../components/Layout/MainLayout';
6// import removed: TenantSelector
7
8export default function PurchaseOrders() {
9 const token = localStorage.getItem('token');
10 let tenantId = '';
11 if (token) {
12 try {
13 const payload = token.split('.')[1];
14 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
15 tenantId = decoded.tenant_id || decoded.tenantId || '';
16 } catch { }
17 }
18 const navigate = useNavigate();
19 const [items, setItems] = useState([]);
20 const [total, setTotal] = useState(0);
21 const [page, setPage] = useState(1);
22 const [limit] = useState(25);
23 const [status, setStatus] = useState('');
24 const [search, setSearch] = useState('');
25 const [loading, setLoading] = useState(true);
26 const [error, setError] = useState(null);
27 const [isRootTenant, setIsRootTenant] = useState(false);
28 const [tenantMap, setTenantMap] = useState({});
29 const [isAdmin, setIsAdmin] = useState(false);
30
31 // Detect if user is admin
32 useEffect(() => {
33 const token = localStorage.getItem('token');
34 if (token) {
35 try {
36 const payload = token.split('.')[1];
37 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
38 setIsAdmin(decoded.role === 'admin');
39 } catch { }
40 }
41 }, []);
42
43 // Detect if user is root tenant (MSP)
44 useEffect(() => {
45 const detectRootTenant = async () => {
46 try {
47 const token = localStorage.getItem('token');
48 const res = await apiFetch('/tenants', {
49 headers: { Authorization: `Bearer ${token}` }
50 });
51 if (res.ok) {
52 setIsRootTenant(true);
53 const data = await res.json();
54 const map = {};
55 data.tenants?.forEach(t => {
56 map[t.tenant_id] = t.name;
57 });
58 setTenantMap(map);
59 }
60 } catch (err) {
61 // Silently fail - 403 is expected for non-MSP users
62 }
63 };
64 detectRootTenant();
65 }, []);
66
67 useEffect(() => {
68 fetchList(tenantId);
69 }, [page, status, search, tenantId]);
70 useEffect(() => {
71 fetchList(tenantId);
72 }, [page, status, search, tenantId]);
73
74 async function fetchList(fetchTenantId = tenantId) {
75 setLoading(true);
76 try {
77 const token = localStorage.getItem('token');
78 let url = '/purchase-orders';
79 const params = new URLSearchParams();
80 params.set('page', page);
81 params.set('limit', limit);
82 if (status) params.set('status', status);
83 if (search) params.set('search', search);
84 const queryString = params.toString();
85 if (queryString) url += `?${queryString}`;
86 const res = await apiFetch(url, { headers: { Authorization: `Bearer ${token}` } });
87 if (!res.ok) throw new Error('Failed to load');
88 const data = await res.json();
89 setItems(data.purchase_orders || []);
90 setTotal(data.total || 0);
91 setError(null);
92 } catch (err) {
93 setError(err.message || 'Failed to load');
94 } finally {
95 setLoading(false);
96 }
97 }
98
99 async function handleDelete(po) {
100 if (!confirm(`Are you sure you want to delete purchase order ${po.po_number || 'this item'}? This action cannot be undone.`)) {
101 return;
102 }
103
104 try {
105 const token = localStorage.getItem('token');
106 const res = await apiFetch(`/purchase-orders/${po.purchase_order_id}`, {
107 method: 'DELETE',
108 headers: { Authorization: `Bearer ${token}` }
109 });
110
111 if (!res.ok) {
112 const data = await res.json().catch(() => ({}));
113 throw new Error(data.error || 'Failed to delete');
114 }
115
116 // Refresh the list
117 fetchList(tenantId);
118 } catch (err) {
119 alert(`Delete failed: ${err.message}`);
120 }
121 }
122
123 return (
124 <MainLayout>
125
126 <div className="page-content">
127 <div className="page-header">
128 <div className="header-content">
129 <h2>Purchase Orders</h2>
130 <div className="header-actions">
131 <div className="search-box">
132 <span className="material-symbols-outlined">search</span>
133 <input placeholder="Search by PO number or supplier" value={search} onChange={e => { setSearch(e.target.value); setPage(1); }} />
134 </div>
135 <select value={status} onChange={e => { setStatus(e.target.value); setPage(1); }}>
136 <option value="">All Statuses</option>
137 <option value="draft">Draft</option>
138 <option value="submitted">Submitted</option>
139 <option value="received">Received</option>
140 <option value="cancelled">Cancelled</option>
141 </select>
142 <button className="btn primary" onClick={() => navigate('/purchase-orders/new')}>
143 <span className="material-symbols-outlined">add</span>
144 New Purchase Order
145 </button>
146 </div>
147 </div>
148 </div>
149
150 {error && <div className="error-message">{error}</div>}
151 {loading ? <div className="loading">Loading...</div> : (
152 <div>
153 <table className="data-table">
154 <thead>
155 <tr>
156 <th>PO Number</th>
157 {isRootTenant && <th>Tenant</th>}
158 <th>Supplier</th>
159 <th>Status</th>
160 <th>Related Customer</th>
161 <th>Created</th>
162 <th>Actions</th>
163 </tr>
164 </thead>
165 <tbody>
166 {items.map(po => (
167 <tr key={po.purchase_order_id}>
168 <td>{po.po_number || '-'}</td>
169 {isRootTenant && <td>{po.tenant_id === 1 ? 'Root Tenant' : (po.tenant_name || tenantMap[po.tenant_id] || '-')}</td>}
170 <td>{po.supplier || '-'}</td>
171 <td><span className={`status-badge status-${po.status}`}>{po.status}</span></td>
172 <td>{po.related_customer_id || '-'}</td>
173 <td>{new Date(po.created_at).toLocaleString()}</td>
174 <td>
175 <button className="btn" onClick={() => navigate(`/purchase-orders/${po.purchase_order_id}`)}>
176 <span className="material-symbols-outlined">visibility</span>
177 </button>
178 <button className="btn" onClick={() => navigate(`/purchase-orders/${po.purchase_order_id}/edit`)}>
179 <span className="material-symbols-outlined">edit</span>
180 </button>
181 {isAdmin && (
182 <button
183 className="btn"
184 onClick={() => handleDelete(po)}
185 style={{ color: '#f44336' }}
186 title="Delete (Admin Only)"
187 >
188 <span className="material-symbols-outlined">delete</span>
189 </button>
190 )}
191 </td>
192 </tr>
193 ))}
194 </tbody>
195 </table>
196
197 <div className="pagination">
198 <button className="btn" disabled={page <= 1} onClick={() => setPage(p => Math.max(1, p - 1))}>
199 <span className="material-symbols-outlined">navigate_before</span>
200 </button>
201 <span>Page {page} of {Math.ceil(total / limit) || 1}</span>
202 <button className="btn" disabled={items.length < limit} onClick={() => setPage(p => p + 1)}>
203 <span className="material-symbols-outlined">navigate_next</span>
204 </button>
205 </div>
206 </div>
207 )}
208 </div>
209 </MainLayout>
210 );
211}