1import { useState, useEffect } from 'react';
2import { useNavigate, useParams } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4// import removed: TenantSelector
5import './ProductForm.css';
6import { apiFetch } from '../lib/api';
8export default function ProductForm() {
9 const navigate = useNavigate();
10 const { id } = useParams();
11 const [selectedTenantId, setSelectedTenantId] = useState('');
12 const [isRootAdmin, setIsRootAdmin] = useState(false);
13 const [form, setForm] = useState({
28 const [loading, setLoading] = useState(false);
29 const [loadingData, setLoadingData] = useState(false);
30 const [error, setError] = useState(null);
33 // Check if user is root admin
34 const token = localStorage.getItem('token');
37 const payload = token.split('.')[1];
38 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
39 const isRoot = decoded.subdomain === 'admin' || decoded.is_msp === true;
40 setIsRootAdmin(isRoot);
46 if (id) fetchProduct();
47 }, [id, selectedTenantId]);
49 const getTenantHeaders = async (baseHeaders = {}) => {
50 const headers = { ...baseHeaders };
51 if (selectedTenantId) {
52 const token = localStorage.getItem('token');
53 const tenantRes = await apiFetch('/tenants', {
55 'Authorization': `Bearer ${token}`,
56 'X-Tenant-Subdomain': 'admin'
60 const data = await tenantRes.json();
61 const tenants = Array.isArray(data) ? data : (data.tenants || []);
62 const tenant = tenants.find(t => t.tenant_id === parseInt(selectedTenantId));
64 headers['X-Tenant-Subdomain'] = tenant.subdomain;
71 const fetchProduct = async () => {
74 const token = localStorage.getItem('token');
75 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
76 const res = await apiFetch(`/products/${id}`, { headers });
77 if (!res.ok) throw new Error('Failed to load');
78 const data = await res.json();
80 // Auto-select the tenant if the product belongs to a different tenant
81 if (data.tenant_id && !selectedTenantId) {
82 setSelectedTenantId(String(data.tenant_id));
86 name: data.name || '',
87 description: data.description || '',
88 supplier: data.supplier || '',
89 is_service: data.is_service,
90 divisible: data.divisible,
91 is_stock: data.is_stock,
92 stock_quantity: data.stock_quantity || 0,
93 min_stock: data.min_stock || 0,
94 price_retail: data.price_retail || 0,
95 price_ex_tax: data.price_ex_tax || 0,
98 supplier_code: data.supplier_code || ''
101 setError(err.message || 'Error');
102 } finally { setLoadingData(false); }
105 const handleSave = async () => {
108 const token = localStorage.getItem('token');
109 const headers = await getTenantHeaders({
110 'Content-Type': 'application/json',
111 Authorization: `Bearer ${token}`
113 const url = id ? `/products/${id}` : '/products';
114 const method = id ? 'PUT' : 'POST';
115 const res = await apiFetch(url, { method, headers, body: JSON.stringify(form) });
116 if (!res.ok) throw new Error('Save failed');
117 navigate('/products');
119 setError(err.message || 'Save error');
120 } finally { setLoading(false); }
123 const handleDelete = async () => {
124 if (!confirm(`Are you sure you want to delete "${form.name}"? This action cannot be undone.`)) {
130 const res = await apiFetch(`/products/${id}`, {
132 credentials: 'include'
135 if (res.status === 204) {
136 // Success - navigate back to products list
137 navigate('/products');
139 // Try to parse JSON error message
141 const errorData = await res.json();
142 setError(errorData.message || errorData.error || 'Failed to delete product');
144 const errorText = await res.text();
145 setError(errorText || 'Failed to delete product');
149 console.error('[ProductForm] Delete error:', err);
150 setError('Failed to delete product: ' + (err.message || err));
158 <div className="page-content">
159 <div className="page-header">
160 <h2>{id ? 'Edit Product' : 'New Product'}</h2>
161 <div className="header-actions">
162 <button className="btn btn-secondary" onClick={() => navigate('/products')}>
163 <span className="material-symbols-outlined">arrow_back</span>
166 {id && isRootAdmin && (
168 className="btn btn-danger"
169 onClick={handleDelete}
171 style={{ marginLeft: '8px' }}
173 <span className="material-symbols-outlined">delete</span>
180 {error && <div className="alert alert-error">{error}</div>}
184 <div className="loading-state">
185 <span className="material-symbols-outlined spinning">progress_activity</span>
186 <p>Loading product...</p>
189 <div className="product-form-container">
190 <div className="form-card">
191 <div className="card-header">
193 <span className="material-symbols-outlined">inventory_2</span>
197 <div className="card-body">
198 <div className="form-group">
199 <label htmlFor="name">Product Name *</label>
204 onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
205 placeholder="Enter product name"
210 <div className="form-group">
211 <label htmlFor="description">Description</label>
214 value={form.description}
215 onChange={e => setForm(f => ({ ...f, description: e.target.value }))}
216 placeholder="Enter product description"
221 <div className="form-group">
222 <label htmlFor="supplier">Supplier</label>
226 value={form.supplier}
227 onChange={e => setForm(f => ({ ...f, supplier: e.target.value }))}
228 placeholder="Enter supplier name"
232 <div className="form-group">
233 <label htmlFor="type">Product Type</label>
236 value={form.is_service ? 'service' : 'product'}
237 onChange={e => setForm(f => ({ ...f, is_service: e.target.value === 'service' }))}
239 <option value="product">Physical Product</option>
240 <option value="service">Service</option>
246 <div className="form-card">
247 <div className="card-header">
249 <span className="material-symbols-outlined">barcode</span>
253 <div className="card-body">
254 <div className="form-row">
255 <div className="form-group">
256 <label htmlFor="upc">UPC</label>
261 onChange={e => setForm(f => ({ ...f, upc: e.target.value }))}
262 placeholder="Universal Product Code"
266 <div className="form-group">
267 <label htmlFor="ean">EAN</label>
272 onChange={e => setForm(f => ({ ...f, ean: e.target.value }))}
273 placeholder="European Article Number"
278 <div className="form-group">
279 <label htmlFor="supplier_code">Supplier Code</label>
283 value={form.supplier_code}
284 onChange={e => setForm(f => ({ ...f, supplier_code: e.target.value }))}
285 placeholder="Supplier's product code"
291 <div className="form-card">
292 <div className="card-header">
294 <span className="material-symbols-outlined">warehouse</span>
298 <div className="card-body">
299 <div className="form-group checkbox-group">
300 <label className="checkbox-label">
303 checked={form.divisible}
304 onChange={e => setForm(f => ({ ...f, divisible: e.target.checked }))}
306 <span>Divisible (can be sold in fractions)</span>
310 <div className="form-group checkbox-group">
311 <label className="checkbox-label">
314 checked={form.is_stock}
315 onChange={e => setForm(f => ({ ...f, is_stock: e.target.checked }))}
317 <span>Track inventory for this product</span>
322 <div className="form-row">
323 <div className="form-group">
324 <label htmlFor="stock_quantity">Current Stock</label>
328 value={form.stock_quantity}
329 onChange={e => setForm(f => ({ ...f, stock_quantity: parseInt(e.target.value || 0, 10) }))}
334 <div className="form-group">
335 <label htmlFor="min_stock">Minimum Stock Level</label>
339 value={form.min_stock}
340 onChange={e => setForm(f => ({ ...f, min_stock: parseInt(e.target.value || 0, 10) }))}
349 <div className="form-card">
350 <div className="card-header">
352 <span className="material-symbols-outlined">payments</span>
356 <div className="card-body">
357 <div className="form-row">
358 <div className="form-group">
359 <label htmlFor="price_retail">Retail Price</label>
360 <div className="input-with-prefix">
361 <span className="input-prefix">$</span>
366 value={form.price_retail}
367 onChange={e => setForm(f => ({ ...f, price_retail: parseFloat(e.target.value || 0) }))}
374 <div className="form-group">
375 <label htmlFor="price_ex_tax">Price (Ex Tax)</label>
376 <div className="input-with-prefix">
377 <span className="input-prefix">$</span>
382 value={form.price_ex_tax}
383 onChange={e => setForm(f => ({ ...f, price_ex_tax: parseFloat(e.target.value || 0) }))}
393 <div className="form-actions">
394 <button className="btn btn-primary" onClick={handleSave} disabled={loading || !form.name}>
397 <span className="material-symbols-outlined spinning">progress_activity</span>
402 <span className="material-symbols-outlined">save</span>
403 {id ? 'Update Product' : 'Create Product'}
407 <button className="btn btn-secondary" onClick={() => navigate('/products')} disabled={loading}>