658 lines
28 KiB
JavaScript
658 lines
28 KiB
JavaScript
// ── 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
|
||
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();
|