Files
ShooterHub/frontend/js/gears.js
2026-04-02 11:24:30 +02:00

658 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── Gears page logic ──────────────────────────────────────────────────────────
let inventory = []; // UserGear[]
let rigs = []; // Rig[]
// ── Toast helper ─────────────────────────────────────────────────────────────
function showToast(msg, type = 'success') {
const el = document.createElement('div');
el.className = `toast align-items-center text-bg-${type} border-0 show`;
el.innerHTML = `<div class="d-flex"><div class="toast-body">${msg}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
document.getElementById('toastContainer').appendChild(el);
setTimeout(() => el.remove(), 3500);
}
// ── Inventory ─────────────────────────────────────────────────────────────────
function gearTypeLabel(t) {
const map = { firearm:'Firearm', scope:'Scope', suppressor:'Suppressor', bipod:'Bipod', magazine:'Magazine' };
return map[t] || t;
}
function renderInventory() {
const tbody = document.getElementById('invBody');
const wrap = document.getElementById('invTableWrap');
const empty = document.getElementById('invEmpty');
document.getElementById('invSpinner').classList.add('d-none');
if (!inventory.length) {
empty.classList.remove('d-none');
wrap.classList.add('d-none');
return;
}
empty.classList.add('d-none');
wrap.classList.remove('d-none');
tbody.innerHTML = inventory.map(ug => `
<tr>
<td>${ug.nickname || '<span class="text-muted">—</span>'}</td>
<td>${ug.gear_detail.brand}</td>
<td>${ug.gear_detail.model_name}</td>
<td><span class="badge bg-secondary">${gearTypeLabel(ug.gear_detail.gear_type)}</span></td>
<td>${ug.serial_number || '<span class="text-muted">—</span>'}</td>
<td>${ug.purchase_date || '<span class="text-muted">—</span>'}</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary me-1" onclick="openEditGear(${ug.id})">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteGear(${ug.id})">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`).join('');
}
async function loadInventory() {
try {
inventory = await apiGet('/inventory/');
renderInventory();
} catch(e) {
document.getElementById('invSpinner').classList.add('d-none');
showToast('Failed to load inventory', 'danger');
}
}
// ── Add Gear modal ────────────────────────────────────────────────────────────
const addGearModal = new bootstrap.Modal('#addGearModal');
document.getElementById('addGearBtn').addEventListener('click', () => {
document.getElementById('catalogSearch').value = '';
document.getElementById('catalogResults').innerHTML = '';
document.getElementById('gearDetailsForm').classList.add('d-none');
document.getElementById('confirmAddGearBtn').classList.add('d-none');
addGearModal.show();
});
document.getElementById('catalogSearchBtn').addEventListener('click', searchCatalog);
document.getElementById('catalogSearch').addEventListener('keydown', e => {
if (e.key === 'Enter') searchCatalog();
});
async function searchCatalog() {
const q = document.getElementById('catalogSearch').value.trim();
if (!q) return;
const spinner = document.getElementById('catalogSpinner');
const results = document.getElementById('catalogResults');
spinner.classList.remove('d-none');
results.innerHTML = '';
document.getElementById('gearDetailsForm').classList.add('d-none');
document.getElementById('confirmAddGearBtn').classList.add('d-none');
try {
const sq = encodeURIComponent(q);
const [firearms, scopes, suppressors, bipods, magazines] = await Promise.all([
apiFetch(`/gears/firearms/?search=${sq}&page_size=100`).then(r => r.json()),
apiFetch(`/gears/scopes/?search=${sq}&page_size=100`).then(r => r.json()),
apiFetch(`/gears/suppressors/?search=${sq}&page_size=100`).then(r => r.json()),
apiFetch(`/gears/bipods/?search=${sq}&page_size=100`).then(r => r.json()),
apiFetch(`/gears/magazines/?search=${sq}&page_size=100`).then(r => r.json()),
]);
const all = [
...(firearms.results || []).map(g => ({ ...g, gear_type: 'firearm' })),
...(scopes.results || []).map(g => ({ ...g, gear_type: 'scope' })),
...(suppressors.results|| []).map(g => ({ ...g, gear_type: 'suppressor' })),
...(bipods.results || []).map(g => ({ ...g, gear_type: 'bipod' })),
...(magazines.results || []).map(g => ({ ...g, gear_type: 'magazine' })),
];
const statusBadge = g =>
g.status === 'PENDING'
? '<span class="badge bg-warning text-dark ms-1">Pending</span>'
: '';
const esc = s => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
const listHtml = all.length
? `<div class="list-group" id="catalogResultsList">${all.map(g => `
<button type="button" class="list-group-item list-group-item-action"
data-gear-id="${g.id}" data-gear-name="${esc(g.brand)} ${esc(g.model_name)}">
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="badge bg-secondary">${gearTypeLabel(g.gear_type)}</span>
<strong>${esc(g.brand)}</strong> ${esc(g.model_name)}
${g.caliber_detail ? `<span class="text-muted small">(${esc(g.caliber_detail.name)})</span>` : ''}
${statusBadge(g)}
</div>
</button>
`).join('')}</div>`
: '<p class="text-muted mb-0">No results found.</p>';
results.innerHTML = listHtml + `
<div class="mt-3 border-top pt-3">
<a class="small text-primary" href="#" id="toggleSuggestGear">
<i class="bi bi-plus-circle me-1"></i>Not found? Suggest a new gear item
</a>
<div id="suggestGearForm" class="mt-2 d-none"></div>
</div>`;
if (all.length) {
document.getElementById('catalogResultsList').addEventListener('click', e => {
const btn = e.target.closest('[data-gear-id]');
if (!btn) return;
chooseGear(parseInt(btn.dataset.gearId), btn.dataset.gearName);
});
}
document.getElementById('toggleSuggestGear').addEventListener('click', e => {
e.preventDefault();
const box = document.getElementById('suggestGearForm');
if (box.innerHTML) { box.classList.toggle('d-none'); return; }
box.classList.remove('d-none');
box.innerHTML = buildSuggestGearForm();
document.getElementById('suggestGearType').addEventListener('change', onSuggestTypeChange);
document.getElementById('submitSuggestGear').addEventListener('click', submitSuggestGear);
});
} catch(e) {
results.innerHTML = '<p class="text-danger">Search failed.</p>';
} finally {
spinner.classList.add('d-none');
}
}
function chooseGear(id, name) {
document.getElementById('chosenGearId').value = id;
document.getElementById('chosenGearName').textContent = name;
document.getElementById('gearDetailsForm').classList.remove('d-none');
document.getElementById('confirmAddGearBtn').classList.remove('d-none');
document.getElementById('gearNickname').value = '';
document.getElementById('gearSerial').value = '';
document.getElementById('gearPurchase').value = '';
document.getElementById('gearNotes').value = '';
document.getElementById('addGearAlert').classList.add('d-none');
}
document.getElementById('confirmAddGearBtn').addEventListener('click', async () => {
const alertEl = document.getElementById('addGearAlert');
alertEl.classList.add('d-none');
const payload = {
gear: parseInt(document.getElementById('chosenGearId').value),
};
const nick = document.getElementById('gearNickname').value.trim();
const ser = document.getElementById('gearSerial').value.trim();
const pur = document.getElementById('gearPurchase').value;
const not = document.getElementById('gearNotes').value.trim();
if (nick) payload.nickname = nick;
if (ser) payload.serial_number = ser;
if (pur) payload.purchase_date = pur;
if (not) payload.notes = not;
try {
const ug = await apiPost('/inventory/', payload);
inventory.push(ug);
renderInventory();
addGearModal.hide();
showToast('Gear added to inventory!');
} catch(e) {
alertEl.textContent = formatErrors(e.data);
alertEl.classList.remove('d-none');
}
});
// ── Edit UserGear ─────────────────────────────────────────────────────────────
const editGearModal = new bootstrap.Modal('#editGearModal');
function openEditGear(id) {
const ug = inventory.find(g => g.id === id);
if (!ug) return;
document.getElementById('editGearId').value = id;
document.getElementById('editNickname').value = ug.nickname || '';
document.getElementById('editSerial').value = ug.serial_number || '';
document.getElementById('editPurchase').value = ug.purchase_date || '';
document.getElementById('editNotes').value = ug.notes || '';
document.getElementById('editGearAlert').classList.add('d-none');
editGearModal.show();
}
document.getElementById('saveEditGearBtn').addEventListener('click', async () => {
const id = parseInt(document.getElementById('editGearId').value);
const alertEl = document.getElementById('editGearAlert');
alertEl.classList.add('d-none');
try {
const updated = await apiPatch(`/inventory/${id}/`, {
nickname: document.getElementById('editNickname').value.trim(),
serial_number: document.getElementById('editSerial').value.trim(),
purchase_date: document.getElementById('editPurchase').value || null,
notes: document.getElementById('editNotes').value.trim(),
});
const idx = inventory.findIndex(g => g.id === id);
if (idx >= 0) inventory[idx] = updated;
renderInventory();
editGearModal.hide();
showToast('Gear updated!');
} catch(e) {
alertEl.textContent = formatErrors(e.data);
alertEl.classList.remove('d-none');
}
});
async function deleteGear(id) {
if (!confirm('Remove this gear from your inventory?')) return;
try {
await apiDelete(`/inventory/${id}/`);
inventory = inventory.filter(g => g.id !== id);
renderInventory();
showToast('Gear removed.');
} catch(e) {
showToast('Failed to delete gear.', 'danger');
}
}
// ── Rigs ──────────────────────────────────────────────────────────────────────
function renderRigs() {
const container = document.getElementById('rigCards');
const empty = document.getElementById('rigEmpty');
document.getElementById('rigSpinner').classList.add('d-none');
if (!rigs.length) {
empty.classList.remove('d-none');
container.innerHTML = '';
return;
}
empty.classList.add('d-none');
container.innerHTML = rigs.map(rig => `
<div class="col-md-6 col-lg-4">
<div class="card rig-card h-100 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="fw-bold mb-0">${rig.name}</h6>
<span class="badge ${rig.is_public ? 'bg-success' : 'bg-secondary'}">
${rig.is_public ? 'Public' : 'Private'}
</span>
</div>
${rig.description ? `<p class="text-muted small mb-2">${rig.description}</p>` : ''}
<div class="rig-items-list mb-3">
${rig.rig_items.length
? `<ul class="list-unstyled mb-0">${rig.rig_items.map(item => `
<li class="small text-muted">
<i class="bi bi-dot"></i>
${item.user_gear.gear_detail.brand} ${item.user_gear.gear_detail.model_name}
${item.role ? `<span class="text-secondary">(${item.role})</span>` : ''}
</li>`).join('')}</ul>`
: '<p class="text-muted small mb-0">No items yet.</p>'}
</div>
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-sm btn-outline-primary" onclick="openManageItems(${rig.id})">
<i class="bi bi-list-check me-1"></i>Items
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="openEditRig(${rig.id})">
<i class="bi bi-pencil me-1"></i>Edit
</button>
<button class="btn btn-sm ${rig.is_public ? 'btn-outline-secondary' : 'btn-outline-success'}"
onclick="toggleRigPublic(${rig.id})">
<i class="bi bi-${rig.is_public ? 'lock' : 'globe2'} me-1"></i>
${rig.is_public ? 'Make private' : 'Make public'}
</button>
<button class="btn btn-sm btn-outline-danger ms-auto" onclick="deleteRig(${rig.id})">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
`).join('');
}
async function loadRigs() {
try {
rigs = await apiGet('/rigs/');
renderRigs();
} catch(e) {
document.getElementById('rigSpinner').classList.add('d-none');
showToast('Failed to load rigs', 'danger');
}
}
// Create/Edit Rig modal
const rigModal = new bootstrap.Modal('#rigModal');
document.getElementById('createRigBtn').addEventListener('click', () => {
document.getElementById('rigId').value = '';
document.getElementById('rigName').value = '';
document.getElementById('rigDesc').value = '';
document.getElementById('rigPublic').checked = false;
document.getElementById('rigModalTitle').textContent = 'New rig';
document.getElementById('rigModalAlert').classList.add('d-none');
rigModal.show();
});
function openEditRig(id) {
const rig = rigs.find(r => r.id === id);
if (!rig) return;
document.getElementById('rigId').value = id;
document.getElementById('rigName').value = rig.name;
document.getElementById('rigDesc').value = rig.description || '';
document.getElementById('rigPublic').checked = rig.is_public;
document.getElementById('rigModalTitle').textContent = 'Edit rig';
document.getElementById('rigModalAlert').classList.add('d-none');
rigModal.show();
}
document.getElementById('saveRigBtn').addEventListener('click', async () => {
const id = document.getElementById('rigId').value;
const alertEl = document.getElementById('rigModalAlert');
alertEl.classList.add('d-none');
const payload = {
name: document.getElementById('rigName').value.trim(),
description: document.getElementById('rigDesc').value.trim(),
is_public: document.getElementById('rigPublic').checked,
};
if (!payload.name) {
alertEl.textContent = 'Name is required.';
alertEl.classList.remove('d-none');
return;
}
try {
if (id) {
const updated = await apiPatch(`/rigs/${id}/`, payload);
const idx = rigs.findIndex(r => r.id === parseInt(id));
if (idx >= 0) rigs[idx] = { ...rigs[idx], ...updated };
} else {
const created = await apiPost('/rigs/', payload);
rigs.push(created);
}
renderRigs();
rigModal.hide();
showToast(id ? 'Rig updated!' : 'Rig created!');
} catch(e) {
alertEl.textContent = formatErrors(e.data);
alertEl.classList.remove('d-none');
}
});
async function toggleRigPublic(id) {
const rig = rigs.find(r => r.id === id);
if (!rig) return;
try {
const updated = await apiPatch(`/rigs/${id}/`, { is_public: !rig.is_public });
const idx = rigs.findIndex(r => r.id === id);
if (idx >= 0) rigs[idx] = { ...rigs[idx], ...updated };
renderRigs();
showToast(`Rig is now ${updated.is_public ? 'public' : 'private'}.`);
} catch(e) {
showToast('Failed to update rig.', 'danger');
}
}
async function deleteRig(id) {
if (!confirm('Delete this rig?')) return;
try {
await apiDelete(`/rigs/${id}/`);
rigs = rigs.filter(r => r.id !== id);
renderRigs();
showToast('Rig deleted.');
} catch(e) {
showToast('Failed to delete rig.', 'danger');
}
}
// ── Manage Rig Items modal ────────────────────────────────────────────────────
const rigItemsModal = new bootstrap.Modal('#rigItemsModal');
async function openManageItems(rigId) {
const rig = rigs.find(r => r.id === rigId);
if (!rig) return;
document.getElementById('rigItemsRigId').value = rigId;
document.getElementById('rigItemsName').textContent = rig.name;
document.getElementById('rigItemsAlert').classList.add('d-none');
renderRigItemsList(rig);
// Populate inventory dropdown (exclude already-added gear)
const addedIds = rig.rig_items.map(i => i.user_gear.id);
const available = inventory.filter(ug => !addedIds.includes(ug.id));
const sel = document.getElementById('rigItemSelect');
sel.innerHTML = available.length
? available.map(ug => `<option value="${ug.id}">${ug.gear_detail.brand} ${ug.gear_detail.model_name}${ug.nickname ? ' ' + ug.nickname : ''}</option>`).join('')
: '<option disabled>All inventory items already added</option>';
rigItemsModal.show();
}
function renderRigItemsList(rig) {
const list = document.getElementById('rigItemsList');
list.innerHTML = rig.rig_items.length
? rig.rig_items.map(item => `
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>${item.user_gear.gear_detail.brand} ${item.user_gear.gear_detail.model_name}</strong>
${item.role ? `<span class="text-muted ms-2 small">${item.role}</span>` : ''}
</div>
<button class="btn btn-sm btn-outline-danger" onclick="removeRigItem(${rig.id}, ${item.id})">
<i class="bi bi-x-lg"></i>
</button>
</li>`).join('')
: '<li class="list-group-item text-muted">No items yet.</li>';
}
document.getElementById('addRigItemBtn').addEventListener('click', async () => {
const rigId = parseInt(document.getElementById('rigItemsRigId').value);
const ugId = parseInt(document.getElementById('rigItemSelect').value);
const role = document.getElementById('rigItemRole').value.trim();
const alertEl = document.getElementById('rigItemsAlert');
alertEl.classList.add('d-none');
if (!ugId) return;
try {
const item = await apiPost(`/rigs/${rigId}/items/`, { user_gear: ugId, role });
const rig = rigs.find(r => r.id === rigId);
if (rig) {
rig.rig_items.push(item);
renderRigItemsList(rig);
renderRigs();
// Refresh dropdown
const addedIds = rig.rig_items.map(i => i.user_gear.id);
const available = inventory.filter(ug => !addedIds.includes(ug.id));
const sel = document.getElementById('rigItemSelect');
sel.innerHTML = available.length
? available.map(ug => `<option value="${ug.id}">${ug.gear_detail.brand} ${ug.gear_detail.model_name}</option>`).join('')
: '<option disabled>All inventory items already added</option>';
document.getElementById('rigItemRole').value = '';
}
showToast('Item added to rig.');
} catch(e) {
alertEl.textContent = formatErrors(e.data);
alertEl.classList.remove('d-none');
}
});
async function removeRigItem(rigId, itemId) {
try {
await apiDelete(`/rigs/${rigId}/items/${itemId}/`);
const rig = rigs.find(r => r.id === rigId);
if (rig) {
rig.rig_items = rig.rig_items.filter(i => i.id !== itemId);
renderRigItemsList(rig);
renderRigs();
}
showToast('Item removed.');
} catch(e) {
showToast('Failed to remove item.', 'danger');
}
}
// ── Suggest new gear (user submission) ───────────────────────────────────────
function buildSuggestGearForm() {
return `
<div class="card bg-light border-0 p-3">
<p class="small text-muted mb-2">
<i class="bi bi-info-circle me-1"></i>
Your suggestion will be available to you immediately and visible to everyone once an admin verifies it.
</p>
<div class="mb-2">
<select class="form-select form-select-sm" id="suggestGearType">
<option value="">Select type…</option>
<option value="firearm">Firearm</option>
<option value="scope">Scope</option>
<option value="suppressor">Suppressor</option>
<option value="bipod">Bipod</option>
<option value="magazine">Magazine</option>
</select>
</div>
<div class="mb-2"><input type="text" class="form-control form-control-sm" id="sgBrand" placeholder="Brand *"></div>
<div class="mb-2"><input type="text" class="form-control form-control-sm" id="sgModel" placeholder="Model *"></div>
<!-- Firearm extras -->
<div id="sgFirearm" class="d-none">
<div class="mb-2">
<select class="form-select form-select-sm" id="sgFirearmType">
<option value="RIFLE">Rifle</option><option value="PISTOL">Pistol</option>
<option value="SHOTGUN">Shotgun</option><option value="REVOLVER">Revolver</option>
<option value="CARBINE">Carbine</option>
</select>
</div>
<div class="mb-2">
${buildCaliberPickerHtml('sgCaliber', 'sgCaliberSuggest')}
</div>
</div>
<!-- Scope extras -->
<div id="sgScope" class="d-none">
<div class="row g-2 mb-2">
<div class="col"><input type="number" class="form-control form-control-sm" id="sgMagMin" placeholder="Mag min"></div>
<div class="col"><input type="number" class="form-control form-control-sm" id="sgMagMax" placeholder="Mag max"></div>
</div>
<div class="row g-2 mb-2">
<div class="col"><input type="number" class="form-control form-control-sm" id="sgObjDia" placeholder="Objective dia (mm) *" step="0.1"></div>
<div class="col"><input type="number" class="form-control form-control-sm" id="sgTubeDia" placeholder="Tube dia (mm, def 30)" step="0.1"></div>
</div>
<div class="mb-2">
<select class="form-select form-select-sm" id="sgReticle">
<option value="">Reticle type (optional)</option>
<option value="DUPLEX">Duplex</option>
<option value="MILDOT">Mil-Dot</option>
<option value="BDC">BDC</option>
<option value="ILLUMINATED">Illuminated</option>
<option value="ETCHED">Etched Glass</option>
</select>
</div>
<div class="row g-2 mb-2">
<div class="col">
<select class="form-select form-select-sm" id="sgAdjUnit">
<option value="">Adjustment (optional)</option>
<option value="MOA">MOA</option>
<option value="MRAD">MRAD</option>
</select>
</div>
<div class="col">
<select class="form-select form-select-sm" id="sgFocalPlane">
<option value="">Focal plane (optional)</option>
<option value="FFP">FFP</option>
<option value="SFP">SFP</option>
</select>
</div>
</div>
</div>
<!-- Magazine extras -->
<div id="sgMagazine" class="d-none">
<div class="mb-2">
${buildCaliberPickerHtml('sgMagCaliber', 'sgMagCaliberSuggest')}
</div>
<div class="mb-2"><input type="number" class="form-control form-control-sm" id="sgMagCapacity" placeholder="Capacity"></div>
</div>
<!-- Suppressor extras -->
<div id="sgSuppressor" class="d-none">
<div class="mb-2">
${buildCaliberPickerHtml('sgSuppCaliber', 'sgSuppCaliberSuggest')}
</div>
<div class="mb-2"><input type="text" class="form-control form-control-sm" id="sgSuppThread" placeholder="Thread pitch"></div>
</div>
<!-- Bipod extras -->
<div id="sgBipod" class="d-none">
<div class="mb-2"><input type="text" class="form-control form-control-sm" id="sgBipodAttach" placeholder="Attachment type"></div>
</div>
<div id="sgAlert" class="alert alert-danger d-none small py-1 mb-2"></div>
<button class="btn btn-sm btn-primary w-100" id="submitSuggestGear" disabled>
<i class="bi bi-send me-1"></i>Submit suggestion
</button>
</div>`;
}
function onSuggestTypeChange() {
const type = document.getElementById('suggestGearType').value;
['sgFirearm','sgScope','sgSuppressor','sgBipod','sgMagazine'].forEach(id =>
document.getElementById(id)?.classList.add('d-none'));
if (type) {
const key = 'sg' + type.charAt(0).toUpperCase() + type.slice(1);
document.getElementById(key)?.classList.remove('d-none');
document.getElementById('submitSuggestGear').disabled = false;
// Load calibers into the newly-visible caliber select
const caliberSelectId = { firearm: 'sgCaliber', magazine: 'sgMagCaliber', suppressor: 'sgSuppCaliber' }[type];
if (caliberSelectId) {
const sel = document.getElementById(caliberSelectId);
if (sel) loadCalibersIntoSelect(sel);
}
} else {
document.getElementById('submitSuggestGear').disabled = true;
}
}
async function submitSuggestGear() {
const type = document.getElementById('suggestGearType').value;
const alertEl = document.getElementById('sgAlert');
alertEl.classList.add('d-none');
const sv = id => document.getElementById(id)?.value?.trim() || '';
const payload = { brand: sv('sgBrand'), model_name: sv('sgModel') };
if (!payload.brand || !payload.model_name) {
alertEl.textContent = 'Brand and model are required.';
alertEl.classList.remove('d-none');
return;
}
if (type === 'firearm') {
payload.firearm_type = sv('sgFirearmType') || 'RIFLE';
const cal = sv('sgCaliber'); if (cal) payload.caliber = parseInt(cal);
} else if (type === 'scope') {
const mn = sv('sgMagMin'), mx = sv('sgMagMax');
if (mn) payload.magnification_min = parseFloat(mn);
if (mx) payload.magnification_max = parseFloat(mx);
const ob = sv('sgObjDia'), tu = sv('sgTubeDia');
if (ob) payload.objective_diameter_mm = parseFloat(ob);
if (tu) payload.tube_diameter_mm = parseFloat(tu);
if (sv('sgReticle')) payload.reticle_type = sv('sgReticle');
if (sv('sgAdjUnit')) payload.adjustment_unit = sv('sgAdjUnit');
if (sv('sgFocalPlane')) payload.focal_plane = sv('sgFocalPlane');
} else if (type === 'magazine') {
const mc = sv('sgMagCaliber'); if (mc) payload.caliber = parseInt(mc);
if (sv('sgMagCapacity')) payload.capacity = parseInt(sv('sgMagCapacity'));
} else if (type === 'suppressor') {
const sc = sv('sgSuppCaliber'); if (sc) payload.max_caliber = parseInt(sc);
if (sv('sgSuppThread')) payload.thread_pitch = sv('sgSuppThread');
} else if (type === 'bipod') {
if (sv('sgBipodAttach')) payload.attachment_type = sv('sgBipodAttach');
}
try {
const created = await apiPost(`/gears/${type}s/`, payload);
// Auto-select the new gear so user can immediately add it to inventory
chooseGear(created.id, `${created.brand} ${created.model_name}`);
showToast('Suggestion submitted! You can now add it to your inventory.');
document.getElementById('suggestGearForm').classList.add('d-none');
} catch(e) {
alertEl.textContent = formatErrors(e.data) || 'Submission failed.';
alertEl.classList.remove('d-none');
}
}
// ── Init ──────────────────────────────────────────────────────────────────────
loadInventory();
loadRigs();