fix: show location (country, city, district) in product edit modal

- JSON endpoint now joins locations+categories+subcategories for full info
- Edit form shows location tags (country, city, district) at top
- Location hidden when adding new product (no location yet)
- All fields properly filled by fillEditForm on edit
- Category and subcategory side by side in edit form
This commit is contained in:
NW
2026-06-23 21:48:53 +01:00
parent e00071b18a
commit 293236921c
4 changed files with 60 additions and 17 deletions

View File

@@ -438,6 +438,26 @@ pre { font-size: 0.8rem; white-space: pre-wrap; word-break: break-all; max-width
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.pf-location-row {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.pf-loc-tag {
display: inline-block;
padding: 0.2rem 0.5rem;
background: #e0e7ff;
color: #3730a3;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
}
.pf-location {
margin-bottom: 0.25rem;
}
.form-row { .form-row {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;

View File

@@ -48,7 +48,16 @@ router.post('/products/:id/delete', async (req, res) => {
}); });
router.get('/products/:id/json', async (req, res) => { router.get('/products/:id/json', async (req, res) => {
const p = await db.getAsync('SELECT * FROM products WHERE id=?', [req.params.id]); const p = await db.getAsync(
`SELECT p.*, l.country, l.city, l.district, c.name as category_name,
COALESCE(sc.name, '') as subcategory_name
FROM products p
LEFT JOIN locations l ON p.location_id = l.id
LEFT JOIN categories c ON p.category_id = c.id
LEFT JOIN subcategories sc ON p.subcategory_id = sc.id
WHERE p.id = ?`,
[req.params.id]
);
if (!p) return res.status(404).json({ error: 'Not found' }); if (!p) return res.status(404).json({ error: 'Not found' });
res.json(p); res.json(p);
}); });

View File

@@ -75,7 +75,7 @@ document.querySelectorAll('.tree-toggle').forEach(el=>{el.addEventListener('clic
const loc=el.dataset.loc, cat=el.dataset.cat, sub=el.dataset.sub, all=el.dataset.all; const loc=el.dataset.loc, cat=el.dataset.cat, sub=el.dataset.sub, all=el.dataset.all;
if(loc||cat||sub||all){ let u='/catalog?'; if(loc) u+='loc='+loc; if(cat) u+='cat='+cat; if(sub) u+='sub='+sub; location.href=u; } if(loc||cat||sub||all){ let u='/catalog?'; if(loc) u+='loc='+loc; if(cat) u+='cat='+cat; if(sub) u+='sub='+sub; location.href=u; }
})}); })});
function openAdd(){ document.getElementById('modal-body').innerHTML=addFormTpl; document.getElementById('product-modal').style.display='flex'; } function openAdd(){ document.getElementById('modal-body').innerHTML=addFormTpl; const loc=document.getElementById('pf-location'); if(loc)loc.style.display='none'; document.getElementById('product-modal').style.display='flex'; }
async function openEdit(id){ const r=await fetch('/catalog/products/'+id+'/json'); const p=await r.json(); async function openEdit(id){ const r=await fetch('/catalog/products/'+id+'/json'); const p=await r.json();
document.getElementById('modal-body').innerHTML=editFormTpl.replace('/__ID__/','/'+p.id+'/'); fillEditForm(p); document.getElementById('product-modal').style.display='flex'; } document.getElementById('modal-body').innerHTML=editFormTpl.replace('/__ID__/','/'+p.id+'/'); fillEditForm(p); document.getElementById('product-modal').style.display='flex'; }
function fillEditForm(p){ const f=document.getElementById('product-modal').querySelector('form'); if(!f)return; function fillEditForm(p){ const f=document.getElementById('product-modal').querySelector('form'); if(!f)return;
@@ -84,7 +84,13 @@ function fillEditForm(p){ const f=document.getElementById('product-modal').query
f.querySelector('[name=photo_url]').value=p.photo_url||''; f.querySelector('[name=hidden_photo_url]').value=p.hidden_photo_url||''; f.querySelector('[name=photo_url]').value=p.photo_url||''; f.querySelector('[name=hidden_photo_url]').value=p.hidden_photo_url||'';
f.querySelector('[name=hidden_coordinates]').value=p.hidden_coordinates||''; f.querySelector('[name=hidden_description]').value=p.hidden_description||''; f.querySelector('[name=hidden_coordinates]').value=p.hidden_coordinates||''; f.querySelector('[name=hidden_description]').value=p.hidden_description||'';
f.querySelector('[name=private_data]').value=p.private_data||''; f.querySelector('[name=category_id]').value=p.category_id||''; f.querySelector('[name=private_data]').value=p.private_data||''; f.querySelector('[name=category_id]').value=p.category_id||'';
updateSubcats(p.category_id,p.subcategory_id); } updateSubcats(p.category_id,p.subcategory_id);
const loc=document.getElementById('pf-location');
if(loc&&(p.country||p.city||p.district)){
loc.style.display=''; document.getElementById('pf-country').textContent=p.country||'';
document.getElementById('pf-city').textContent=p.city||''; document.getElementById('pf-district').textContent=p.district||'';
} else if(loc){loc.style.display='none';}
}
function updateSubcats(catId,selSub){ const ss=document.getElementById('product-modal').querySelector('[name=subcategory_id]'); if(!ss)return; function updateSubcats(catId,selSub){ const ss=document.getElementById('product-modal').querySelector('[name=subcategory_id]'); if(!ss)return;
ss.innerHTML='<option value="">-- Subcategory --</option>'; subcats.forEach(s=>{if(s.category_id==catId){const o=document.createElement('option');o.value=s.id;o.textContent=s.name;if(s.id==selSub)o.selected=true;ss.appendChild(o)}}); } ss.innerHTML='<option value="">-- Subcategory --</option>'; subcats.forEach(s=>{if(s.category_id==catId){const o=document.createElement('option');o.value=s.id;o.textContent=s.name;if(s.id==selSub)o.selected=true;ss.appendChild(o)}}); }
document.getElementById('product-modal').addEventListener('click',e=>{if(e.target===document.getElementById('product-modal'))document.getElementById('product-modal').style.display='none'}); document.getElementById('product-modal').addEventListener('click',e=>{if(e.target===document.getElementById('product-modal'))document.getElementById('product-modal').style.display='none'});

View File

@@ -3,6 +3,14 @@ export function renderProductEditForm(action, catOptions, subcatJson) {
const title = isEdit ? 'Edit Product' : 'Add Product'; const title = isEdit ? 'Edit Product' : 'Add Product';
return `<h2>${title}</h2> return `<h2>${title}</h2>
<form method="POST" action="${action}" enctype="multipart/form-data" class="product-form"> <form method="POST" action="${action}" enctype="multipart/form-data" class="product-form">
<div class="pf-group pf-location" id="pf-location" style="display:none">
<label>Location</label>
<div class="pf-location-row">
<span class="pf-loc-tag" id="pf-country"></span>
<span class="pf-loc-tag" id="pf-city"></span>
<span class="pf-loc-tag" id="pf-district"></span>
</div>
</div>
<div class="pf-group"> <div class="pf-group">
<label>Name</label> <label>Name</label>
<input name="name" required placeholder="Product name"> <input name="name" required placeholder="Product name">
@@ -17,15 +25,17 @@ export function renderProductEditForm(action, catOptions, subcatJson) {
<input name="quantity_in_stock" type="number" min="0" value="0" placeholder="0"> <input name="quantity_in_stock" type="number" min="0" value="0" placeholder="0">
</div> </div>
</div> </div>
<div class="pf-group"> <div class="pf-row">
<label>Category</label> <div class="pf-group">
<select name="category_id" required onchange="updateSubcats(this.value)"> <label>Category</label>
<option value="">-- Select --</option>${catOptions} <select name="category_id" required onchange="updateSubcats(this.value)">
</select> <option value="">-- Select --</option>${catOptions}
</div> </select>
<div class="pf-group"> </div>
<label>Subcategory</label> <div class="pf-group">
<select name="subcategory_id"><option value="">-- Subcategory --</option></select> <label>Subcategory</label>
<select name="subcategory_id"><option value="">-- Subcategory --</option></select>
</div>
</div> </div>
<div class="pf-group"> <div class="pf-group">
<label>Description</label> <label>Description</label>
@@ -49,11 +59,9 @@ export function renderProductEditForm(action, catOptions, subcatJson) {
<label>Or Upload Hidden Photo</label> <label>Or Upload Hidden Photo</label>
<input type="file" name="hidden_photo_file" accept="image/*" class="pf-file"> <input type="file" name="hidden_photo_file" accept="image/*" class="pf-file">
</div> </div>
<div class="pf-row"> <div class="pf-group">
<div class="pf-group"> <label>Hidden Coordinates</label>
<label>Hidden Coordinates</label> <input name="hidden_coordinates" placeholder="lat,lng">
<input name="hidden_coordinates" placeholder="lat,lng">
</div>
</div> </div>
<div class="pf-group"> <div class="pf-group">
<label>Hidden Description</label> <label>Hidden Description</label>