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
195 lines
7.5 KiB
HTML
195 lines
7.5 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Setări{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="page-head">
|
|
<div>
|
|
<h1>Setări</h1>
|
|
<p class="subtitle">
|
|
Datele firmei, cântarul și imprimanta; de aici ajungi la evidențele aplicației.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<nav class="hub-grid" aria-label="Administrare">
|
|
<a class="hub-card" href="{{ url_for('components.index') }}">
|
|
<span class="hub-title">Componente</span>
|
|
<span class="hub-meta">{{ components|length }} definite · ce se cântărește</span>
|
|
</a>
|
|
<a class="hub-card" href="{{ url_for('recipes.index') }}">
|
|
<span class="hub-title">Produse finite</span>
|
|
<span class="hub-meta">{{ recipes|length }} rețete · combinații de componente</span>
|
|
</a>
|
|
<a class="hub-card" href="{{ url_for('label_templates.index') }}">
|
|
<span class="hub-title">Șabloane</span>
|
|
<span class="hub-meta">{{ templates|length }} șabloane · design de etichetă</span>
|
|
</a>
|
|
<a class="hub-card" href="{{ url_for('fields.index') }}">
|
|
<span class="hub-title">Câmpuri</span>
|
|
<span class="hub-meta">{{ fields_count }}
|
|
{{ 'variabilă' if fields_count == 1 else 'variabile' }} · text pentru etichete</span>
|
|
</a>
|
|
<a class="hub-card" href="{{ url_for('history.index') }}">
|
|
<span class="hub-title">Istoric</span>
|
|
<span class="hub-meta">Pachete finalizate, filtre și export CSV</span>
|
|
</a>
|
|
<a class="hub-card" href="{{ url_for('monitor.index') }}">
|
|
<span class="hub-title">Monitor serial</span>
|
|
<span class="hub-meta">Identifică un cântar nou</span>
|
|
</a>
|
|
</nav>
|
|
|
|
<div class="grid settings-grid">
|
|
<section class="card">
|
|
<form method="post" class="form">
|
|
<h2>Date firmă</h2>
|
|
<div class="field">
|
|
<label for="company_name">Denumire firmă</label>
|
|
<input id="company_name" name="company_name" type="text" maxlength="200"
|
|
value="{{ app_settings.company_name }}">
|
|
</div>
|
|
<div class="field">
|
|
<label for="company_address">Adresă</label>
|
|
<input id="company_address" name="company_address" type="text" maxlength="200"
|
|
value="{{ app_settings.company_address }}">
|
|
</div>
|
|
<div class="field">
|
|
<label for="company_phone">Telefon</label>
|
|
<input id="company_phone" name="company_phone" type="text" maxlength="200"
|
|
value="{{ app_settings.company_phone }}">
|
|
</div>
|
|
|
|
<h2>Cântar</h2>
|
|
<div class="field">
|
|
<label for="active_scale_profile">Profil cântar activ</label>
|
|
<select id="active_scale_profile" name="active_scale_profile">
|
|
<option value="">— introducere manuală —</option>
|
|
{% for p in profiles %}
|
|
<option value="{{ p.name }}" {{ 'selected' if app_settings.active_scale_profile == p.name }}>
|
|
{{ p.label }} · {{ p.port }} · {{ p.protocol }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% if not profiles %}
|
|
<p class="muted small">
|
|
Nu există profile. Configurează cântarul în SyncPulse Scale Bridge sau folosește
|
|
<a href="{{ url_for('monitor.index') }}">monitorul serial</a>.
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<h2>Etichete</h2>
|
|
<div class="field">
|
|
<label for="default_template_id">Șablon implicit</label>
|
|
<select id="default_template_id" name="default_template_id">
|
|
<option value="">— primul disponibil —</option>
|
|
{% for t in templates %}
|
|
<option value="{{ t.id }}" {{ 'selected' if app_settings.default_template_id == t.id|string }}>
|
|
{{ t.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="default_operator">Operator implicit</label>
|
|
<input id="default_operator" name="default_operator" type="text" maxlength="64"
|
|
value="{{ app_settings.default_operator }}">
|
|
</div>
|
|
<label class="check">
|
|
<input type="checkbox" name="auto_print" {{ 'checked' if app_settings.auto_print == '1' }}>
|
|
Tipărește automat eticheta după ultima componentă
|
|
</label>
|
|
|
|
<div class="actions">
|
|
<button class="btn primary" type="submit">Salvează setările</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
|
|
<aside class="card help-card">
|
|
<h2>Starea cântarului</h2>
|
|
<p>
|
|
{% if scale_available %}
|
|
<span class="badge ok">bibliotecă încărcată</span>
|
|
{% else %}
|
|
<span class="badge err">indisponibil</span>
|
|
{% endif %}
|
|
</p>
|
|
{% if scale_error %}<p class="message error">{{ scale_error }}</p>{% endif %}
|
|
|
|
<h2>Profile detectate</h2>
|
|
{% if profiles %}
|
|
<ul class="profile-list">
|
|
{% for p in profiles %}
|
|
<li>
|
|
<strong>{{ p.label }}</strong><br>
|
|
<span class="muted">{{ p.name }} · {{ p.port }} · {{ p.protocol }}</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="muted">Niciun profil salvat în <code>instance/bridge.json</code>.</p>
|
|
{% endif %}
|
|
|
|
<h2>Imprimantă (QZ Tray)</h2>
|
|
<p class="muted small">
|
|
Eticheta este generată de aplicație și tipărită pe imprimanta termică prin QZ Tray, separat de
|
|
cântar. Biblioteca din <code>static/js/vendor/qz-tray.js</code> este patch-uită pentru
|
|
autentificare cu pairing key.
|
|
</p>
|
|
<p class="qz-state-line">
|
|
<span class="badge" data-qz-status>QZ Tray: se verifică…</span>
|
|
<span class="muted small" data-qz-version></span>
|
|
</p>
|
|
<div class="field">
|
|
<label for="qz-printer">Imprimantă implicită</label>
|
|
<select id="qz-printer" data-qz-printer>
|
|
<option value="">Implicit (sistem)</option>
|
|
</select>
|
|
</div>
|
|
<div class="actions">
|
|
<button class="btn" type="button" data-qz-connect>Conectează</button>
|
|
<button class="btn" type="button" data-qz-refresh>Reîncarcă lista</button>
|
|
<button class="btn ghost" type="button" data-qz-test>Tipărește test</button>
|
|
</div>
|
|
|
|
<h2>Integrare</h2>
|
|
<p class="muted small">
|
|
Aplicația importă direct pachetul <code>vendor/syncpulse-scale-bridge</code>; profilul serial
|
|
este citit din <code>instance/bridge.json</code>.
|
|
</p>
|
|
<p class="muted small">
|
|
Pentru teste fără hardware: <code>CANTARIRE_SIMULATE=mahak_mds</code>.
|
|
</p>
|
|
|
|
<h2>Sistem</h2>
|
|
<p class="small">
|
|
{% if setup.ready %}<span class="badge ok">gata de cântărit</span>
|
|
{% else %}<span class="badge warn">{{ setup.missing|length }} de completat</span>{% endif %}
|
|
<span class="badge">schema v{{ db_status.schema_version }}</span>
|
|
</p>
|
|
<ul class="system-list small">
|
|
<li><span class="muted">Bază de date</span> <code>{{ db_status.path }}</code></li>
|
|
<li><span class="muted">Mărime</span> {{ db_status.size_human }}</li>
|
|
<li><span class="muted">Integritate</span> {{ db_status.integrity }}</li>
|
|
<li><span class="muted">Înregistrări</span>
|
|
{{ db_status.counts.components }} componente ·
|
|
{{ db_status.counts.recipes }} produse finite ·
|
|
{{ db_status.counts.packages }} pachete</li>
|
|
</ul>
|
|
<div class="actions">
|
|
<a class="btn" href="{{ url_for('main.settings_backup') }}">Descarcă backup</a>
|
|
<a class="btn ghost" href="{{ url_for('main.healthz') }}" target="_blank" rel="noopener">Stare (/healthz)</a>
|
|
</div>
|
|
{% if backups %}
|
|
<p class="muted small">Ultimele backup-uri:</p>
|
|
<ul class="system-list small">
|
|
{% for b in backups %}
|
|
<li>{{ b.created_at.strftime('%d.%m.%Y %H:%M') }} · {{ b.size_human }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
</aside>
|
|
</div>
|
|
{% endblock %}
|