feat: barcode scan button + reminder in manual product form

- Add 📷 scan button next to the barcode field in product form
  Opens a camera modal (BarcodeDetector if available, manual fallback)
  Detects barcode after 2 consistent frames, fills field and closes modal
- Show ⚠️ hint below the barcode field when it's empty (new products only):
  'Aggiungi il barcode così al prossimo acquisto basta scansionarlo!'
  Hint hides automatically when a code is entered or scanned
- Hint is hidden in edit-product mode (barcode already saved)
- scanBarcodeForForm() reuses the modal overlay; handles camera permission
  errors gracefully (shows manual input only)
This commit is contained in:
dadaloop82
2026-04-07 12:17:18 +00:00
parent b7ed9899fa
commit 4e576559a9
5 changed files with 110 additions and 2 deletions
+103
View File
@@ -2425,6 +2425,10 @@ function startManualEntry(barcode = '') {
document.getElementById('product-form-title').textContent = 'Nuovo Prodotto'; document.getElementById('product-form-title').textContent = 'Nuovo Prodotto';
const pfAiRow = document.getElementById('pf-ai-fill-row'); const pfAiRow = document.getElementById('pf-ai-fill-row');
if (pfAiRow) pfAiRow.style.display = 'block'; if (pfAiRow) pfAiRow.style.display = 'block';
// Show barcode hint when no barcode was passed
_updateBarcodeHint();
document.getElementById('pf-barcode').addEventListener('input', _updateBarcodeHint);
// Remove datalist/autocomplete suggestions for new products (they cause confusion) // Remove datalist/autocomplete suggestions for new products (they cause confusion)
document.getElementById('pf-name').removeAttribute('list'); document.getElementById('pf-name').removeAttribute('list');
@@ -2552,6 +2556,102 @@ function onPfUnitChange() {
if (confRow) confRow.style.display = unit === 'conf' ? 'block' : 'none'; if (confRow) confRow.style.display = unit === 'conf' ? 'block' : 'none';
} }
function _updateBarcodeHint() {
const hint = document.getElementById('pf-barcode-hint');
const val = (document.getElementById('pf-barcode')?.value || '').trim();
if (hint) hint.style.display = val ? 'none' : 'block';
}
/**
* Open a temporary camera modal to scan a barcode and fill the pf-barcode field.
* Uses BarcodeDetector if available, otherwise shows manual-input fallback.
*/
async function scanBarcodeForForm() {
const overlayEl = document.getElementById('modal-overlay');
const contentEl = document.getElementById('modal-content');
let stream = null;
let scanning = true;
const stopStream = () => {
scanning = false;
if (stream) stream.getTracks().forEach(t => t.stop());
stream = null;
};
const closeScanner = () => {
stopStream();
overlayEl.style.display = 'none';
};
contentEl.innerHTML = `
<div class="modal-header">
<h3>🔖 Scansiona Barcode</h3>
<button class="modal-close" onclick="document.getElementById('modal-overlay').style.display='none'"></button>
</div>
<div style="position:relative;width:100%;background:#000;border-radius:10px;overflow:hidden;aspect-ratio:4/3">
<video id="pf-bc-video" autoplay playsinline muted style="width:100%;height:100%;object-fit:cover"></video>
<div class="scanner-line scanning" style="position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);height:2px;background:rgba(59,130,246,0.8)"></div>
</div>
<p style="text-align:center;margin-top:12px;color:var(--text-muted);font-size:0.88rem">Inquadra il codice a barre del prodotto</p>
<div style="margin-top:10px;text-align:center">
<input type="text" id="pf-bc-manual" class="form-input" placeholder="O inserisci manualmente..." inputmode="numeric" style="max-width:260px;display:inline-block">
<button class="btn btn-primary" style="margin-top:8px;width:100%" onclick="
const v = document.getElementById('pf-bc-manual').value.trim();
if(v){ document.getElementById('pf-barcode').value=v; _updateBarcodeHint(); document.getElementById('modal-overlay').style.display='none'; }
"> Usa questo codice</button>
</div>
`;
overlayEl.style.display = 'flex';
// Attach close handler (clicking backdrop)
overlayEl.onclick = (e) => { if (e.target === overlayEl) { stopStream(); overlayEl.style.display = 'none'; overlayEl.onclick = null; } };
try {
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
const video = document.getElementById('pf-bc-video');
video.srcObject = stream;
await video.play();
if (!('BarcodeDetector' in window)) {
// No native API — just let user type manually
return;
}
const detector = new BarcodeDetector({ formats: ['ean_13','ean_8','code_128','code_39','upc_a','upc_e'] });
const detectionHistory = {};
const scanFrame = async () => {
if (!scanning || !stream) return;
try {
const barcodes = await detector.detect(video);
if (barcodes.length > 0) {
const code = barcodes[0].rawValue;
detectionHistory[code] = (detectionHistory[code] || 0) + 1;
if (detectionHistory[code] >= 2) {
scanning = false;
stopStream();
overlayEl.style.display = 'none';
overlayEl.onclick = null;
document.getElementById('pf-barcode').value = code;
_updateBarcodeHint();
if (navigator.vibrate) navigator.vibrate(80);
showToast(`🔖 Barcode acquisito: ${code}`, 'success');
return;
}
}
} catch (_) {}
if (scanning) requestAnimationFrame(scanFrame);
};
requestAnimationFrame(scanFrame);
} catch (err) {
// Camera not available — user can still type manually
const videoEl = document.getElementById('pf-bc-video');
if (videoEl) videoEl.style.display = 'none';
}
}
async function submitProduct(e) { async function submitProduct(e) {
e.preventDefault(); e.preventDefault();
showLoading(true); showLoading(true);
@@ -2870,6 +2970,9 @@ function editProductFromAction() {
document.getElementById('product-form-title').textContent = 'Modifica Prodotto'; document.getElementById('product-form-title').textContent = 'Modifica Prodotto';
const pfAiRow = document.getElementById('pf-ai-fill-row'); const pfAiRow = document.getElementById('pf-ai-fill-row');
if (pfAiRow) pfAiRow.style.display = 'none'; if (pfAiRow) pfAiRow.style.display = 'none';
// Keep barcode hint hidden in edit mode
const pfBcHint = document.getElementById('pf-barcode-hint');
if (pfBcHint) pfBcHint.style.display = 'none';
// Restore datalist for editing (was removed for new products) // Restore datalist for editing (was removed for new products)
document.getElementById('pf-name').setAttribute('list', 'common-products'); document.getElementById('pf-name').setAttribute('list', 'common-products');
+1
View File
@@ -1802,3 +1802,4 @@
[2026-04-07 12:00:02] OK — 31 items cached [2026-04-07 12:00:02] OK — 31 items cached
[2026-04-07 12:05:02] OK — 31 items cached [2026-04-07 12:05:02] OK — 31 items cached
[2026-04-07 12:10:01] OK — 30 items cached [2026-04-07 12:10:01] OK — 30 items cached
[2026-04-07 12:15:02] OK — 30 items cached
BIN
View File
Binary file not shown.
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -472,7 +472,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>🔖 Barcode</label> <label>🔖 Barcode</label>
<input type="text" id="pf-barcode" class="form-input" placeholder="Codice a barre (se disponibile)"> <div class="expiry-input-row">
<input type="text" id="pf-barcode" class="form-input" placeholder="Codice a barre (se disponibile)" inputmode="numeric">
<button type="button" class="btn btn-accent btn-scan-expiry" id="pf-barcode-scan-btn" onclick="scanBarcodeForForm()" title="Scansiona barcode">📷</button>
</div>
<p class="form-hint" id="pf-barcode-hint" style="display:none">⚠️ Aggiungi il barcode così al prossimo acquisto basta scansionarlo!</p>
</div> </div>
<input type="hidden" id="pf-image"> <input type="hidden" id="pf-image">
<div class="product-image-preview" id="pf-image-preview" style="display:none"> <div class="product-image-preview" id="pf-image-preview" style="display:none">