Add mobile batch inventory scanning
This commit is contained in:
+7
-2
@@ -77,9 +77,14 @@ function customerRow(c) { return `<tr><td class="customer"><strong>${esc(c.name)
|
||||
|
||||
function receiveForm() {
|
||||
if(!state.models.length){openModal('<h2>No active models</h2><p>Add or reactivate a product model from the Admin page before receiving inventory.</p><div class="form-actions"><button class="primary" data-cancel>Close</button></div>');$("[data-cancel]").onclick=closeModal;return;}
|
||||
openModal(`<h2>Receive product</h2><p>Add a vSeeBox unit to available inventory.</p><form id="receiveForm"><div class="scan"><label>UID<input name="uid" autofocus></label><label>Serial number<input name="sn"></label><label>MAC address<input name="mac"></label></div><div class="form-row"><label>Model<select name="model" required>${state.models.map(m=>`<option value="${esc(m.name)}">${esc(m.name)}</option>`).join("")}</select></label><label>Condition<select name="condition"><option>New</option><option>Used</option><option>Refurbished</option></select></label></div><div class="form-row"><label>Received date<input name="receivedAt" type="date" value="${today()}" required></label><label>Purchase cost<input name="cost" type="number" min="0" step=".01"></label></div><label>Notes<textarea name="notes" rows="2"></textarea></label><div class="form-actions"><button type="button" class="secondary" data-cancel>Cancel</button><button class="primary">Receive product</button></div></form>`);
|
||||
$("#receiveForm").onsubmit=e=>submitForm(e,"/api/products","Product received."); $("[data-cancel]").onclick=closeModal;
|
||||
openModal(`<h2>Receive inventory</h2><p>Enter the shared batch details, then scan each device label. The three barcodes are added as one device at a time.</p><form id="receiveForm"><div class="form-row"><label>Quantity<input name="quantity" type="number" min="1" max="100" value="1" required></label><label>Model<select name="model" required>${state.models.map(m=>`<option value="${esc(m.name)}">${esc(m.name)}</option>`).join("")}</select></label></div><div class="form-row"><label>Condition<select name="condition"><option>New</option><option>Used</option><option>Refurbished</option></select></label><label>Received date<input name="receivedAt" type="date" value="${today()}" required></label></div><div class="form-row"><label>Purchase cost per device<input name="cost" type="number" min="0" step=".01"></label><label>Notes<textarea name="notes" rows="1"></textarea></label></div><section class="scan-panel"><div class="scan-panel-heading"><h3>Current device</h3><span data-scan-progress>0 of 1 added</span></div><div class="scan"><label>UID<input name="uid" autocomplete="off" autofocus></label><label>Serial number<input name="sn" autocomplete="off"></label><label>MAC address<input name="mac" autocomplete="off"></label></div><button type="button" class="secondary scan-camera">Scan label with camera</button><button type="button" class="secondary add-device">Add device to batch</button><p class="field-help scan-help">Scan the three barcodes on one label, or enter them manually. Duplicate identifiers are rejected.</p></section><div class="batch-list" data-batch-list><p class="empty-batch">No devices added yet.</p></div><div class="form-actions"><button type="button" class="secondary" data-cancel>Cancel</button><button class="primary">Save batch</button></div></form>`);
|
||||
const form=$("#receiveForm"),batch=[],quantity=form.elements.quantity,progress=form.querySelector("[data-scan-progress]"),list=form.querySelector("[data-batch-list]"),help=form.querySelector(".scan-help");
|
||||
const duplicate=(row)=>[row.uid,row.sn,row.mac].filter(Boolean).some(value=>{const key=value.trim().toLowerCase();return batch.some(x=>[x.uid,x.sn,x.mac].some(y=>y.trim().toLowerCase()===key))||state.products.some(x=>[x.uid,x.sn,x.mac].some(y=>y&&y.trim().toLowerCase()===key))});
|
||||
const renderBatch=()=>{progress.textContent=`${batch.length} of ${quantity.value||1} added`;list.innerHTML=batch.length?batch.map((x,i)=>`<div class="batch-row"><strong>#${i+1}</strong><span>UID ${esc(x.uid||"—")}</span><span>SN ${esc(x.sn||"—")}</span><span>MAC ${esc(x.mac||"—")}</span><button type="button" class="danger" data-remove-batch="${i}">Remove</button></div>`).join(""):'<p class="empty-batch">No devices added yet.</p>';form.querySelector("button.primary").disabled=batch.length!==Number(quantity.value||1)};
|
||||
form.elements.quantity.oninput=()=>{if(Number(quantity.value)>100)quantity.value=100;renderBatch()};form.querySelector(".add-device").onclick=()=>{const row={uid:form.elements.uid.value.trim(),sn:form.elements.sn.value.trim(),mac:form.elements.mac.value.trim()};if(!row.uid&&!row.sn&&!row.mac){help.textContent="Enter or scan at least one identifier.";return}if(duplicate(row)){help.textContent="Duplicate identifier detected. Scan a different device.";return}if(batch.length>=Number(quantity.value)){help.textContent="The requested quantity is already filled.";return}batch.push(row);form.elements.uid.value=form.elements.sn.value=form.elements.mac.value="";help.textContent="Device added. Scan the next label.";renderBatch();form.elements.uid.focus()};list.onclick=e=>{const button=e.target.closest("[data-remove-batch]");if(button){batch.splice(Number(button.dataset.removeBatch),1);renderBatch()}};
|
||||
form.querySelector(".scan-camera").onclick=()=>scanLabel(form,help);renderBatch();form.onsubmit=async e=>{e.preventDefault();if(batch.length!==Number(quantity.value)){help.textContent=`Add all ${quantity.value} devices before saving.`;return}const data=Object.fromEntries(new FormData(form));delete data.quantity;delete data.uid;delete data.sn;delete data.mac;data.items=batch;await change("/api/products/batch","POST",data,`${batch.length} ${batch.length===1?"product":"products"} received.`)};$("[data-cancel]").onclick=closeModal;
|
||||
}
|
||||
async function scanLabel(form,help){if(!("BarcodeDetector" in window)){help.textContent="Camera barcode scanning is not supported by this browser. Enter the values manually or use a supported mobile browser.";return}if(!navigator.mediaDevices?.getUserMedia){help.textContent="Camera access is unavailable. Use manual entry or enable camera access for this site.";return}let stream;try{stream=await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:"environment"}}});const detector=new BarcodeDetector({formats:["code_128","code_39","ean_13","ean_8","itf"]}),video=document.createElement("video");video.autoplay=true;video.playsInline=true;video.srcObject=stream;const overlay=document.createElement("div");overlay.className="scanner-overlay";overlay.innerHTML='<div><strong>Point at the full label</strong><small>Hold steady while the three barcodes are read.</small><button type="button">Cancel</button></div>';overlay.prepend(video);document.body.append(overlay);let cancelled=false;const finish=()=>{cancelled=true;stream.getTracks().forEach(track=>track.stop());overlay.remove()};overlay.querySelector("button").onclick=finish;await video.play();let started=Date.now();while(Date.now()-started<15000&&!cancelled){const codes=await detector.detect(video),values=codes.map(x=>String(x.rawValue||"").trim()).filter(Boolean);for(const value of values){const clean=value.replace(/^\s*(UID|SN|MAC)\s*[:#]?\s*/i,"");if(/^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(clean))form.elements.mac.value=clean;else if(/^V\w/i.test(clean))form.elements.sn.value=clean;else if(!form.elements.uid.value)form.elements.uid.value=clean;else if(!form.elements.sn.value)form.elements.sn.value=clean;else if(!form.elements.mac.value)form.elements.mac.value=clean}if(form.elements.uid.value&&form.elements.sn.value&&form.elements.mac.value){help.textContent="All three identifiers scanned. Add this device to the batch.";break}await new Promise(resolve=>setTimeout(resolve,120))}finish()}catch(error){if(stream)stream.getTracks().forEach(track=>track.stop());help.textContent="Unable to read that label. Try better lighting or enter the identifiers manually."}}
|
||||
function sellForm(id="") {
|
||||
const available=state.products.filter(p=>p.status==="available");
|
||||
if(!available.length){openModal('<h2>No available inventory</h2><p>Receive a product before recording a sale.</p><div class="form-actions"><button class="primary" data-cancel>Close</button></div>');$("[data-cancel]").onclick=closeModal;return;}
|
||||
|
||||
Reference in New Issue
Block a user