// ── 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 = `
${msg}
`; 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 => ` ${ug.nickname || ''} ${ug.gear_detail.brand} ${ug.gear_detail.model_name} ${gearTypeLabel(ug.gear_detail.gear_type)} ${ug.serial_number || ''} ${ug.purchase_date || ''} `).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' ? 'Pending' : ''; const esc = s => String(s ?? '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); const listHtml = all.length ? `
${all.map(g => ` `).join('')}
` : '

No results found.

'; results.innerHTML = listHtml + `
Not found? Suggest a new gear item
`; 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 = '

Search failed.

'; } 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 => `
${rig.name}
${rig.is_public ? 'Public' : 'Private'}
${rig.description ? `

${rig.description}

` : ''}
${rig.rig_items.length ? `
    ${rig.rig_items.map(item => `
  • ${item.user_gear.gear_detail.brand} ${item.user_gear.gear_detail.model_name} ${item.role ? `(${item.role})` : ''}
  • `).join('')}
` : '

No items yet.

'}
`).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 => ``).join('') : ''; rigItemsModal.show(); } function renderRigItemsList(rig) { const list = document.getElementById('rigItemsList'); list.innerHTML = rig.rig_items.length ? rig.rig_items.map(item => `
  • ${item.user_gear.gear_detail.brand} ${item.user_gear.gear_detail.model_name} ${item.role ? `${item.role}` : ''}
  • `).join('') : '
  • No items yet.
  • '; } 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 => ``).join('') : ''; 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 `

    Your suggestion will be available to you immediately and visible to everyone once an admin verifies it.

    ${buildCaliberPickerHtml('sgCaliber', 'sgCaliberSuggest')}
    ${buildCaliberPickerHtml('sgMagCaliber', 'sgMagCaliberSuggest')}
    ${buildCaliberPickerHtml('sgSuppCaliber', 'sgSuppCaliberSuggest')}
    `; } 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();