bff6d70b17
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
160 lines
5.6 KiB
HTML
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 %}
|