Files
ske087 bff6d70b17 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
2026-10-18 23:38:45 +03:00

160 lines
5.6 KiB
HTML

{% extends "base.html" %}
{% block title %}Cântărire produs finit{% endblock %}
{% block content %}
<section class="page-head">
<div>
<h1>Cântărire produs finit</h1>
<p class="subtitle">Alege produsul, cântărește componentele în ordine și tipărește eticheta.</p>
</div>
<div class="stats">
<div class="stat"><span class="stat-value" id="stat-today-count">{{ stats.today_count }}</span><span class="stat-label">pachete azi</span></div>
<div class="stat"><span class="stat-value">{{ stats.today_total }}</span><span class="stat-label">total azi</span></div>
<div class="stat"><span class="stat-value">{{ stats.recipes }}</span><span class="stat-label">produse finite</span></div>
</div>
</section>
{% if not setup.ready %}
<section class="card setup-card">
<div class="setup-head">
<div>
<h2>Pregătire pentru prima cântărire</h2>
<p class="muted small">Completează pe rând; poți cântări după ce lista e goală.</p>
</div>
<span class="badge warn">{{ setup.missing|length }} de completat</span>
</div>
<ul class="setup-list">
{% for item in setup.checks %}
{% if not item.ok and not item.optional %}
<li class="setup-item">
<span class="setup-mark" aria-hidden="true">!</span>
<div class="setup-body">
<strong>{{ item.label }}</strong>
<span class="muted small">{{ item.message }}</span>
</div>
{% if item.url %}
<a class="btn ghost" href="{{ item.url }}">{{ item.action or 'Deschide' }}</a>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
</section>
{% endif %}
<div class="grid weigh-grid">
<section class="card flow-card">
<div class="flow-head">
<h2 id="flow-title">Pachet nou</h2>
<span id="flow-progress" class="chip"></span>
</div>
<div id="flow-body" class="flow-body"></div>
<p id="flow-message" class="message" role="status" aria-live="polite"></p>
</section>
<section class="card scale-card">
<div class="scale-head">
<span id="scale-dot" class="dot off"></span>
<span id="scale-state">{{ "Se conectează…" if scale_ready else "Introducere manuală" }}</span>
<span id="scale-profile" class="chip">{{ active_profile or "fără profil" }}</span>
</div>
<div class="weight-display">
<span id="weight-value">--</span>
<span id="weight-unit" class="weight-unit"></span>
</div>
<div class="weight-meta">
<span id="stability" class="badge">—</span>
<span id="age" class="muted"></span>
<span id="raw" class="raw"></span>
</div>
<p id="scale-error" class="message error">{% if scale_error %}{{ scale_error }}{% endif %}</p>
{% if profiles %}
<div class="scale-hint">
<span>Profil:</span>
<select id="profile-select" class="inline-select">
{% for p in profiles %}
<option value="{{ p.name }}" {{ 'selected' if p.name == active_profile }}>
{{ p.label }} · {{ p.port }} · {{ p.protocol }}
</option>
{% endfor %}
</select>
</div>
{% else %}
<div class="scale-hint">
<span>Cântarul nu este configurat — cântărește manual.</span>
<a class="link" href="{{ url_for('monitor.index') }}">Monitor</a>
</div>
{% endif %}
<div class="qz-hint">
<span>Imprimantă:</span>
<select class="inline-select" data-qz-printer>
<option value="">Implicit (sistem)</option>
</select>
<button class="link" type="button" data-qz-connect>Conectează</button>
<button class="link" type="button" data-qz-test>Test</button>
<span class="muted small" data-qz-version></span>
</div>
</section>
</div>
<section class="card">
<div class="card-head">
<h2>Ultimele pachete</h2>
<a class="link" href="{{ url_for('history.index') }}">Vezi tot istoricul →</a>
</div>
{% if recent %}
<div class="table-wrap">
<table class="table compact">
<thead>
<tr><th>Finalizat</th><th>Produs finit</th><th>Componente</th><th class="num">Total</th><th>Lot</th><th>Tipărit</th><th></th></tr>
</thead>
<tbody>
{% for p in recent %}
<tr>
<td class="nowrap">{{ (p.completed_at or p.created_at)|dt }}</td>
<td>{{ p.recipe_code }} · {{ p.recipe_name }}</td>
<td class="muted">{{ p.item_summary or "-" }}</td>
<td class="num">{{ (p.total_weight or 0)|weight }} {{ p.unit }}</td>
<td>{{ p.lot or "-" }}</td>
<td>{{ "da" if p.printed_at else "nu" }}</td>
<td class="right">
{% if p.status == "done" %}
<a class="link" target="_blank" rel="noopener"
href="{{ url_for('printing.print_label', package_id=p.id, auto='0') }}">Etichetă</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="muted">Niciun pachet finalizat încă.</p>
{% endif %}
</section>
<iframe id="print-frame" class="print-frame" title="Tipărire etichetă" aria-hidden="true"></iframe>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
{% endblock %}
{% block scripts %}
<script>
window.CANTARIRE = {
pollMs: {{ poll_ms|int }},
scaleReady: {{ 'true' if scale_ready else 'false' }},
activeProfile: {{ active_profile|tojson }},
defaultOperator: {{ default_operator|tojson }},
defaultTemplateId: {{ default_template_id|tojson }},
autoPrint: {{ 'true' if auto_print else 'false' }},
initial: {{ initial|tojson }},
recipes: {{ recipes|tojson }}
};
</script>
<script src="{{ url_for('static', filename='js/weigh.js') }}"></script>
{% endblock %}