Files
ShooterHub/frontend/js/gears.js

658 lines
28 KiB
JavaScript
Raw Normal View History

// ── 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();