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:
2026-10-18 23:38:45 +03:00
commit bff6d70b17
107 changed files with 21428 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Etichetă {{ payload.fields.recipe_code }} · {{ payload.fields.total_weight }} {{ payload.fields.unit }}</title>
<style>
@page { size: {{ payload.template.width_mm }}mm {{ payload.template.height_mm }}mm; margin: 0; }
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: #e9edf1; }
.preview-bar {
position: sticky; top: 0; z-index: 2;
display: flex; align-items: center; gap: 12px;
padding: 10px 16px; background: #0f172a; color: #fff;
font: 14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.preview-bar button {
margin-left: auto; padding: 8px 16px; border: 0; border-radius: 6px;
background: #14b8a6; color: #04201d; font-weight: 700; cursor: pointer;
}
.preview-stage { padding: 24px; display: flex; justify-content: center; }
.label-sheet {
width: {{ payload.template.width_mm }}mm;
height: {{ payload.template.height_mm }}mm;
background: #fff;
overflow: hidden;
box-shadow: 0 6px 24px rgba(15, 23, 42, .18);
}
{{ payload.css|safe }}
@media print {
html, body { background: #fff; }
.preview-bar, .preview-stage { padding: 0; }
.preview-bar { display: none !important; }
.label-sheet { box-shadow: none; margin: 0; }
}
</style>
</head>
<body>
{% if preview %}
<div class="preview-bar">
<strong>Previzualizare șablon</strong>
<span>{{ payload.template.name }} · {{ payload.template.width_mm|weight }} × {{ payload.template.height_mm|weight }} mm</span>
<button type="button" onclick="window.print()">Tipărește previzualizarea</button>
</div>
{% endif %}
<div class="preview-stage">
<div class="label-sheet" id="label-sheet">
{{ payload.html|safe }}
</div>
</div>
<script id="label-payload" type="application/json">{{ payload|tojson }}</script>
<script src="{{ url_for('static', filename='js/vendor/qz-tray.js') }}"></script>
<script src="{{ url_for('static', filename='js/qz-print.js') }}"></script>
<script>
(function () {
var payload = JSON.parse(document.getElementById('label-payload').textContent);
var auto = {{ 'true' if auto else 'false' }};
var preview = {{ 'true' if preview else 'false' }};
function acknowledge() {
if (preview) return;
try {
fetch(payload.ack_url, { method: 'POST', headers: { 'Accept': 'application/json' } });
} catch (e) { /* tipărirea a reușit; confirmarea este auxiliară */ }
}
function doPrint() {
if (window.CantarirePrint && typeof window.CantarirePrint.print === 'function') {
Promise.resolve(window.CantarirePrint.print(payload)).then(function (result) {
if (result && result.method === 'qz') acknowledge();
}, function (err) {
console.error('Tipărirea QZ Tray a eșuat:', err);
});
return;
}
window.print();
acknowledge();
}
window.addEventListener('load', function () {
if (!auto) return;
setTimeout(doPrint, 120);
});
})();
</script>
</body>
</html>