Initial commit: aplicatie de cantarire si etichetare
Aplicatie Flask + SQLite pentru productie alimentara: cantarirea componentelor unui produs finit si tiparirea etichetei prin QZ Tray. - produse finite (retete) formate din componente cantarite - designer vizual de etichete (mm/px, zona sigura, campuri variabile, cod QR cu continut din mai multe bucati) - legare produs finit -> sablon de eticheta - navigare pe doua taburi: Cantarire / Setari - administrare: componente, produse finite, sabloane, campuri, istoric - backup, resetare baza de date, checklist de pregatire, /healthz - CLI de administrare si serviciu systemd pentru productie - vendor/syncpulse-scale-bridge (GPL-3.0-or-later) pentru citirea cantarului
This commit is contained in:
@@ -0,0 +1,577 @@
|
||||
/* Fluxul de cântărire a unui produs finit din 1..N componente.
|
||||
*
|
||||
* State machine:
|
||||
* fără pachet -> alegi produsul finit și începi
|
||||
* pas curent -> cântărești componenta (cântar sau manual), Enter acceptă
|
||||
* ultimul pas -> serverul finalizează pachetul și întoarce eticheta
|
||||
* finalizat -> eticheta se tipărește; Enter începe următorul pachet
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var cfg = window.CANTARIRE || {};
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
|
||||
// Polling: rapid cat timp se cantareste, rar cand pagina doar asteapta.
|
||||
var POLL_ACTIVE_MS = Number(cfg.pollMs) || 700;
|
||||
var POLL_IDLE_MS = 2500;
|
||||
var pollTimer = null;
|
||||
var el = {
|
||||
flow: $('flow-body'),
|
||||
title: $('flow-title'),
|
||||
progress: $('flow-progress'),
|
||||
message: $('flow-message'),
|
||||
weightValue: $('weight-value'),
|
||||
weightUnit: $('weight-unit'),
|
||||
stability: $('stability'),
|
||||
age: $('age'),
|
||||
raw: $('raw'),
|
||||
scaleDot: $('scale-dot'),
|
||||
scaleState: $('scale-state'),
|
||||
scaleProfile: $('scale-profile'),
|
||||
scaleError: $('scale-error'),
|
||||
profileSelect: $('profile-select'),
|
||||
toast: $('toast'),
|
||||
frame: $('print-frame')
|
||||
};
|
||||
|
||||
var state = cfg.initial || null;
|
||||
var recipes = cfg.recipes || [];
|
||||
var activeProfile = cfg.activeProfile || '';
|
||||
var scaleReady = Boolean(cfg.scaleReady);
|
||||
var manualMode = !scaleReady;
|
||||
var busy = false;
|
||||
var polling = false;
|
||||
var reading = null;
|
||||
var forceArmedAt = 0;
|
||||
var lastLabel = null;
|
||||
var lastRecipeId = null;
|
||||
var lastLot = '';
|
||||
var lastOperator = cfg.defaultOperator || '';
|
||||
var toastTimer = null;
|
||||
|
||||
// -- utilitare -----------------------------------------------------------
|
||||
function esc(value) {
|
||||
return String(value === null || value === undefined ? '' : value)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function fmt(value) {
|
||||
var number = Number(value);
|
||||
if (!isFinite(number)) return '--';
|
||||
var text = number.toFixed(3);
|
||||
return text.replace(/\.?0+$/, '') || '0';
|
||||
}
|
||||
|
||||
function message(text, kind) {
|
||||
if (!el.message) return;
|
||||
el.message.textContent = text || '';
|
||||
el.message.className = 'message ' + (kind || '');
|
||||
}
|
||||
|
||||
function toast(text, kind) {
|
||||
if (!el.toast) return;
|
||||
el.toast.textContent = text;
|
||||
el.toast.className = 'toast show ' + (kind || '');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(function () { el.toast.className = 'toast ' + (kind || ''); }, 3200);
|
||||
}
|
||||
|
||||
function setProgress(text) {
|
||||
if (el.progress) el.progress.textContent = text || '';
|
||||
}
|
||||
|
||||
function getJSON(url) {
|
||||
return fetch(url, { headers: { Accept: 'application/json' }, cache: 'no-store' })
|
||||
.then(function (response) { return response.json(); });
|
||||
}
|
||||
|
||||
function postJSON(url, body) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(body || {})
|
||||
}).then(function (response) {
|
||||
return response.json().then(function (data) {
|
||||
return { status: response.status, data: data };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setBusy(value) {
|
||||
busy = value;
|
||||
document.querySelectorAll('#flow-body button, #flow-body input, #flow-body select')
|
||||
.forEach(function (node) { node.disabled = value; });
|
||||
}
|
||||
|
||||
// -- stare ---------------------------------------------------------------
|
||||
function adopt(data) {
|
||||
if (!data) return;
|
||||
if (data.state === null) {
|
||||
state = null;
|
||||
} else if (data.package) {
|
||||
state = {
|
||||
package: data.package,
|
||||
recipe: data.recipe,
|
||||
items: data.items || [],
|
||||
step: data.step,
|
||||
planned_total: data.planned_total,
|
||||
total_weight: data.total_weight,
|
||||
complete: Boolean(data.complete),
|
||||
open: Boolean(data.open)
|
||||
};
|
||||
if (data.recipe) lastRecipeId = data.recipe.id;
|
||||
if (data.package) {
|
||||
lastLot = data.package.lot || lastLot;
|
||||
lastOperator = data.package.operator || lastOperator;
|
||||
}
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
var data = await getJSON('/api/package');
|
||||
adopt(data);
|
||||
} catch (error) {
|
||||
message('Nu pot citi starea pachetului: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// -- randare -------------------------------------------------------------
|
||||
function render() {
|
||||
if (!el.flow) return;
|
||||
if (!state) {
|
||||
renderStart();
|
||||
} else if (state.complete) {
|
||||
renderDone();
|
||||
} else {
|
||||
renderStep();
|
||||
}
|
||||
}
|
||||
|
||||
function recipeOptions(selected) {
|
||||
return recipes.map(function (recipe) {
|
||||
var suffix = ' · ' + recipe.components.length + ' comp.';
|
||||
return '<option value="' + recipe.id + '"' + (recipe.id === selected ? ' selected' : '') +
|
||||
'>' + esc(recipe.code + ' · ' + recipe.name) + suffix + '</option>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function breakdown(recipe) {
|
||||
return (recipe.components || []).map(function (part) {
|
||||
return '<li><span class="w">' + (part.target_weight ? fmt(part.target_weight) + ' ' + esc(recipe.unit) : '—')
|
||||
+ '</span> ' + esc(part.name) + '</li>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderStart() {
|
||||
if (el.title) el.title.textContent = 'Pachet nou';
|
||||
setProgress('');
|
||||
|
||||
if (!recipes.length) {
|
||||
el.flow.innerHTML = '<p class="muted">Nu există produse finite active. ' +
|
||||
'<a class="link" href="/retete/nou">Creează unul</a>.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var selected = lastRecipeId || (recipes[0] && recipes[0].id) || null;
|
||||
var recipe = recipes.filter(function (r) { return r.id === selected; })[0] || recipes[0];
|
||||
|
||||
el.flow.innerHTML =
|
||||
'<div class="field">' +
|
||||
' <label for="start-recipe">Produs finit</label>' +
|
||||
' <select id="start-recipe">' + recipeOptions(recipe.id) + '</select>' +
|
||||
'</div>' +
|
||||
'<ul class="plan" id="start-plan">' + breakdown(recipe) + '</ul>' +
|
||||
'<div class="field-row">' +
|
||||
' <div class="field"><label for="start-lot">Lot</label>' +
|
||||
' <input id="start-lot" type="text" maxlength="64" value="' + esc(lastLot) + '" placeholder="ex. LOT-2610-A"></div>' +
|
||||
' <div class="field"><label for="start-operator">Operator</label>' +
|
||||
' <input id="start-operator" type="text" maxlength="64" value="' + esc(lastOperator) + '"></div>' +
|
||||
'</div>' +
|
||||
'<div class="actions">' +
|
||||
' <button class="btn primary" type="button" id="btn-start">Începe pachetul</button>' +
|
||||
'</div>' +
|
||||
'<p class="hint muted small">Enter pornește pachetul. Apoi cântărești fiecare componentă în ordinea de mai sus.</p>';
|
||||
|
||||
var select = $('start-recipe');
|
||||
if (select) {
|
||||
select.addEventListener('change', function () {
|
||||
var chosen = recipes.filter(function (r) { return String(r.id) === select.value; })[0];
|
||||
if (chosen) {
|
||||
lastRecipeId = chosen.id;
|
||||
var plan = $('start-plan');
|
||||
if (plan) plan.innerHTML = breakdown(chosen);
|
||||
}
|
||||
});
|
||||
}
|
||||
var button = $('btn-start');
|
||||
if (button) button.addEventListener('click', startPackage);
|
||||
}
|
||||
|
||||
function renderStep() {
|
||||
var step = state.step;
|
||||
if (!step) { refresh(); return; }
|
||||
if (el.title) el.title.textContent = state.recipe ? state.recipe.name : 'Cântărire';
|
||||
setProgress('Pas ' + step.position + ' din ' + step.total);
|
||||
|
||||
var accepted = (state.items || []).map(function (item) {
|
||||
return '<li><span class="w">' + fmt(item.weight) + ' ' + esc(item.unit) + '</span> ' +
|
||||
esc(item.component_name) + '</li>';
|
||||
}).join('');
|
||||
|
||||
el.flow.innerHTML =
|
||||
'<div class="step">' +
|
||||
' <div class="step-label">Pune pe cântar</div>' +
|
||||
' <div class="step-component">' + esc(step.component_name) + '</div>' +
|
||||
(step.target_weight
|
||||
? '<div class="step-target">Țintă: ' + fmt(step.target_weight) + ' ' + esc(step.unit) + '</div>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
(manualMode
|
||||
? '<div class="field"><label for="manual-weight">Greutate (' + esc(step.unit) + ')</label>' +
|
||||
'<input id="manual-weight" type="number" step="0.001" min="0" inputmode="decimal" placeholder="0">' +
|
||||
'</div>'
|
||||
: '<div class="actions"><button class="btn ghost" type="button" id="btn-manual-toggle">Introducere manuală</button></div>') +
|
||||
'<div class="actions">' +
|
||||
' <button class="btn primary" type="button" id="btn-accept">Acceptă' + (manualMode ? '' : ' (Enter)') + '</button>' +
|
||||
(accepted ? ' <button class="btn" type="button" id="btn-undo">Anulează ultima</button>' : '') +
|
||||
' <button class="btn ghost" type="button" id="btn-void">Renunță la pachet</button>' +
|
||||
'</div>' +
|
||||
(accepted ? '<h3>Acceptate</h3><ul class="plan accepted">' + accepted + '</ul>' : '');
|
||||
|
||||
if (manualMode) {
|
||||
var input = $('manual-weight');
|
||||
if (input) { input.focus(); input.select(); }
|
||||
}
|
||||
var accept = $('btn-accept');
|
||||
if (accept) accept.addEventListener('click', acceptStep);
|
||||
var undo = $('btn-undo');
|
||||
if (undo) undo.addEventListener('click', undoStep);
|
||||
var voidButton = $('btn-void');
|
||||
if (voidButton) voidButton.addEventListener('click', voidPackage);
|
||||
var toggle = $('btn-manual-toggle');
|
||||
if (toggle) {
|
||||
toggle.addEventListener('click', function () {
|
||||
manualMode = true;
|
||||
renderStep();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderDone() {
|
||||
var pkg = state.package || {};
|
||||
if (el.title) el.title.textContent = pkg.recipe_name || 'Pachet finalizat';
|
||||
setProgress('Finalizat');
|
||||
|
||||
var accepted = (state.items || []).map(function (item) {
|
||||
return '<li><span class="w">' + fmt(item.weight) + ' ' + esc(item.unit) + '</span> ' +
|
||||
esc(item.component_name) + '</li>';
|
||||
}).join('');
|
||||
|
||||
el.flow.innerHTML =
|
||||
'<div class="done">' +
|
||||
' <div class="done-mark">✓</div>' +
|
||||
' <div class="done-total"><span class="value">' + fmt(pkg.total_weight) + '</span>' +
|
||||
' <span class="unit">' + esc(pkg.unit) + '</span></div>' +
|
||||
' <div class="muted">Pachet #' + pkg.id + (pkg.lot ? ' · Lot ' + esc(pkg.lot) : '') + '</div>' +
|
||||
'</div>' +
|
||||
'<ul class="plan accepted">' + accepted + '</ul>' +
|
||||
'<div class="actions">' +
|
||||
' <button class="btn primary" type="button" id="btn-next">Pachet nou (Enter)</button>' +
|
||||
(lastLabel ? ' <button class="btn" type="button" id="btn-reprint">Tipărește eticheta</button>' : '') +
|
||||
' <a class="btn ghost" target="_blank" rel="noopener" href="/labels/' + pkg.id + '/print?auto=0">Vezi eticheta</a>' +
|
||||
'</div>';
|
||||
|
||||
var next = $('btn-next');
|
||||
if (next) next.addEventListener('click', startPackage);
|
||||
var reprint = $('btn-reprint');
|
||||
if (reprint) {
|
||||
reprint.addEventListener('click', function () {
|
||||
if (lastLabel) printLabel(lastLabel);
|
||||
});
|
||||
}
|
||||
if (next) next.focus();
|
||||
}
|
||||
|
||||
// -- acțiuni -------------------------------------------------------------
|
||||
async function startPackage() {
|
||||
if (busy) return;
|
||||
var select = $('start-recipe');
|
||||
var recipeId = select ? Number(select.value) : lastRecipeId;
|
||||
if (!recipeId) {
|
||||
toast('Alege produsul finit.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
message('Se pregătește pachetul…', 'info');
|
||||
try {
|
||||
var result = await postJSON('/api/packages', {
|
||||
recipe_id: recipeId,
|
||||
operator: ($('start-operator') ? $('start-operator').value.trim() : '') || lastOperator,
|
||||
lot: ($('start-lot') ? $('start-lot').value.trim() : '') || lastLot,
|
||||
// Serverul alege sablonul: produsul finit -> setarea implicita -> primul sablon.
|
||||
template_id: null
|
||||
});
|
||||
if (!result.data.ok) {
|
||||
message(result.data.error || 'Pachetul nu a putut fi început.', 'error');
|
||||
toast(result.data.error || 'Eroare.', 'error');
|
||||
return;
|
||||
}
|
||||
lastLabel = null;
|
||||
adopt(result.data);
|
||||
message('Pachet început. Cântărește prima componentă.', 'info');
|
||||
} catch (error) {
|
||||
message('Eroare de rețea: ' + error, 'error');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function manualWeight() {
|
||||
var input = $('manual-weight');
|
||||
if (!input) return null;
|
||||
var raw = String(input.value || '').trim().replace(',', '.');
|
||||
return raw === '' ? null : raw;
|
||||
}
|
||||
|
||||
async function acceptStep() {
|
||||
if (busy || !state || !state.package) return;
|
||||
var force = forceArmedAt && Date.now() - forceArmedAt < 5000;
|
||||
var body = {};
|
||||
if (force) body.force = true;
|
||||
if (manualMode) {
|
||||
var value = manualWeight();
|
||||
if (value === null) {
|
||||
toast('Introdu greutatea sau conectează cântarul.', 'error');
|
||||
return;
|
||||
}
|
||||
body.weight = value;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
message('Se înregistrează…', 'info');
|
||||
try {
|
||||
var result = await postJSON('/api/packages/' + state.package.id + '/items', body);
|
||||
var data = result.data;
|
||||
|
||||
if (!result.status || result.status >= 400 || !data.ok) {
|
||||
if (data.reading) applyReading(data.reading);
|
||||
var unstable = /stabil/i.test(data.error || '');
|
||||
if (unstable) {
|
||||
forceArmedAt = Date.now();
|
||||
message((data.error || 'Greutate instabilă.') +
|
||||
' Apasă din nou pe Acceptă pentru a folosi valoarea curentă.', 'error');
|
||||
} else {
|
||||
message(data.error || 'Cântărirea nu a putut fi salvată.', 'error');
|
||||
toast(data.error || 'Eroare la salvare.', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
forceArmedAt = 0;
|
||||
adopt(data);
|
||||
if (data.label) {
|
||||
lastLabel = data.label;
|
||||
if (cfg.autoPrint !== false) {
|
||||
await printLabel(data.label);
|
||||
message('Etichetă tipărită. Apasă Enter pentru următorul pachet.', 'success');
|
||||
} else {
|
||||
message('Pachet finalizat. Apasă „Tipărește eticheta” sau Enter pentru următorul pachet.', 'success');
|
||||
}
|
||||
updateStats();
|
||||
} else {
|
||||
message('Acceptat: ' + fmt(state.total_weight) + ' ' + (state.package.unit || ''), 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
message('Eroare de rețea la salvare.', 'error');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function undoStep() {
|
||||
if (busy || !state || !state.package) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
var result = await postJSON('/api/packages/' + state.package.id + '/undo', {});
|
||||
if (result.data.ok) {
|
||||
adopt(result.data);
|
||||
message('Ultima cântărire a fost anulată.', 'info');
|
||||
} else {
|
||||
message(result.data.error || 'Nu se poate anula.', 'error');
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function voidPackage() {
|
||||
if (busy || !state || !state.package) return;
|
||||
if (!confirm('Renunți la pachetul început? Cântăririle lui nu vor apărea în istoric.')) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await postJSON('/api/packages/' + state.package.id + '/void', {});
|
||||
state = null;
|
||||
lastLabel = null;
|
||||
message('Pachet anulat.', 'info');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
// -- tipărire ------------------------------------------------------------
|
||||
function acknowledge(payload) {
|
||||
if (!payload || !payload.ack_url) return;
|
||||
fetch(payload.ack_url, { method: 'POST', headers: { Accept: 'application/json' } })
|
||||
.catch(function () { /* confirmarea este auxiliară */ });
|
||||
}
|
||||
|
||||
function printLabel(payload) {
|
||||
if (!payload) return Promise.resolve();
|
||||
|
||||
if (window.CantarirePrint && typeof window.CantarirePrint.print === 'function') {
|
||||
return Promise.resolve(window.CantarirePrint.print(payload)).then(function (result) {
|
||||
if (result && result.method === 'qz') acknowledge(payload);
|
||||
return result;
|
||||
}).catch(function (error) {
|
||||
console.error('Tipărirea a eșuat:', error);
|
||||
toast('Imprimanta nu a răspuns.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
if (el.frame) {
|
||||
el.frame.src = payload.print_url + '?auto=1';
|
||||
return Promise.resolve();
|
||||
}
|
||||
window.open(payload.print_url, '_blank', 'noopener');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
var node = $('stat-today-count');
|
||||
if (!node) return;
|
||||
var value = Number(node.textContent) || 0;
|
||||
node.textContent = String(value + 1);
|
||||
}
|
||||
|
||||
// -- citirea cântarului --------------------------------------------------
|
||||
function applyReading(data) {
|
||||
reading = data;
|
||||
if (!data) return;
|
||||
var ok = Boolean(data.ok);
|
||||
var stale = Boolean(data.stale);
|
||||
var stable = Boolean(data.stable);
|
||||
var hasValue = data.value !== null && data.value !== undefined;
|
||||
|
||||
if (el.weightValue) {
|
||||
el.weightValue.textContent = hasValue ? fmt(data.value) : '--';
|
||||
el.weightValue.className = !ok ? 'error' : (stable && !stale ? '' : 'unstable');
|
||||
}
|
||||
if (el.weightUnit) el.weightUnit.textContent = data.unit || '';
|
||||
|
||||
if (!ok) {
|
||||
if (el.scaleDot) el.scaleDot.className = 'dot off';
|
||||
if (el.scaleState) el.scaleState.textContent = 'Cântar indisponibil';
|
||||
if (el.stability) { el.stability.textContent = 'fără date'; el.stability.className = 'badge off'; }
|
||||
if (el.scaleError) el.scaleError.textContent = data.error || 'Cântarul nu poate fi citit.';
|
||||
} else if (stale) {
|
||||
if (el.scaleDot) el.scaleDot.className = 'dot off';
|
||||
if (el.scaleState) el.scaleState.textContent = 'Citire veche';
|
||||
if (el.stability) { el.stability.textContent = 'citire veche'; el.stability.className = 'badge warn'; }
|
||||
if (el.scaleError) el.scaleError.textContent = '';
|
||||
} else {
|
||||
if (el.scaleDot) el.scaleDot.className = 'dot ok';
|
||||
if (el.scaleState) el.scaleState.textContent = 'Cântar conectat';
|
||||
if (el.stability) {
|
||||
el.stability.textContent = stable ? 'stabil' : 'instabil';
|
||||
el.stability.className = 'badge ' + (stable ? 'ok' : 'warn');
|
||||
}
|
||||
if (el.scaleError) el.scaleError.textContent = '';
|
||||
}
|
||||
|
||||
if (el.scaleProfile) el.scaleProfile.textContent = (data.profile || activeProfile || 'fără profil') +
|
||||
(data.port ? ' · ' + data.port : '');
|
||||
if (el.age) {
|
||||
el.age.textContent = typeof data.age_ms === 'number'
|
||||
? (data.age_ms < 1000 ? '<1 s' : (data.age_ms / 1000).toFixed(1) + ' s')
|
||||
: '';
|
||||
}
|
||||
if (el.raw) el.raw.textContent = data.raw_hex ? 'raw: ' + data.raw_hex.slice(0, 40) : '';
|
||||
}
|
||||
|
||||
function poll() {
|
||||
if (!scaleReady || polling || document.hidden) return;
|
||||
polling = true;
|
||||
var controller = new AbortController();
|
||||
var timer = setTimeout(function () { controller.abort(); }, 6000);
|
||||
var params = new URLSearchParams();
|
||||
if (activeProfile) params.set('profile', activeProfile);
|
||||
|
||||
fetch('/api/weight?' + params.toString(), {
|
||||
headers: { Accept: 'application/json' }, cache: 'no-store', signal: controller.signal
|
||||
})
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(applyReading)
|
||||
.catch(function (error) {
|
||||
if (error && error.name !== 'AbortError') {
|
||||
if (el.scaleDot) el.scaleDot.className = 'dot off';
|
||||
if (el.scaleState) el.scaleState.textContent = 'Conexiune întreruptă';
|
||||
}
|
||||
})
|
||||
.finally(function () { clearTimeout(timer); polling = false; });
|
||||
}
|
||||
|
||||
// -- tastatură -----------------------------------------------------------
|
||||
function onKeyDown(event) {
|
||||
if (event.key !== 'Enter' || event.ctrlKey || event.metaKey) return;
|
||||
var tag = (event.target && event.target.tagName || '').toLowerCase();
|
||||
if (tag === 'textarea' || tag === 'button' || tag === 'a') return;
|
||||
|
||||
if (!state) {
|
||||
event.preventDefault();
|
||||
startPackage();
|
||||
} else if (state.complete) {
|
||||
event.preventDefault();
|
||||
startPackage();
|
||||
} else if (state.step) {
|
||||
event.preventDefault();
|
||||
acceptStep();
|
||||
}
|
||||
}
|
||||
|
||||
// -- start ---------------------------------------------------------------
|
||||
function schedulePoll() {
|
||||
clearTimeout(pollTimer);
|
||||
var delay = (state && state.open && state.step) ? POLL_ACTIVE_MS : POLL_IDLE_MS;
|
||||
pollTimer = setTimeout(function () {
|
||||
poll();
|
||||
schedulePoll();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
if (el.profileSelect) {
|
||||
el.profileSelect.addEventListener('change', function () {
|
||||
activeProfile = el.profileSelect.value;
|
||||
poll();
|
||||
});
|
||||
}
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (!document.hidden) poll();
|
||||
});
|
||||
|
||||
render();
|
||||
if (scaleReady) {
|
||||
poll();
|
||||
schedulePoll();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user