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:
@@ -2426,6 +2426,10 @@ function startManualEntry(barcode = '') {
|
|||||||
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');
|
||||||
document.getElementById('pf-brand').removeAttribute('list');
|
document.getElementById('pf-brand').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');
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
+5
-1
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user