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,120 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ "Câmp nou" if is_new else "Editare câmp" }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<h1>{{ "Câmp nou" if is_new else "Editare câmp" }}</h1>
|
||||
<p class="subtitle">
|
||||
Scrie textul etichetei; între acolade poți referi un câmp existent.
|
||||
</p>
|
||||
</div>
|
||||
<a class="btn ghost" href="{{ url_for('fields.index') }}">Înapoi la listă</a>
|
||||
</section>
|
||||
|
||||
<section class="card narrow">
|
||||
<form method="post" class="form">
|
||||
<div class="field">
|
||||
<label for="label">Denumire <span class="req">*</span></label>
|
||||
<input id="label" name="label" type="text" maxlength="{{ max_label }}" required
|
||||
value="{{ field.label or '' }}" placeholder="ex. Contact firmă" autofocus>
|
||||
<p class="muted small">Apare în popup-ul din designer.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="template">Text <span class="req">*</span></label>
|
||||
<textarea id="template" name="template" rows="3" maxlength="{{ max_template }}"
|
||||
placeholder="ex. Tel. {{ '{{company.phone}}' }}">{{ field.template or '' }}</textarea>
|
||||
<p class="muted small">
|
||||
Poți folosi orice câmp din tabelul de mai jos, scris între <code>{{ '{{' }}</code> și
|
||||
<code>{{ '}}' }}</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Previzualizare cu date demo</label>
|
||||
<div id="field-preview" class="field-preview muted">—</div>
|
||||
</div>
|
||||
|
||||
<label class="check">
|
||||
<input type="checkbox" name="active"
|
||||
{{ 'checked' if field.active in [True, 1, '1', 'on', 'true'] or is_new }}>
|
||||
Câmp activ (apare în designer)
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn primary" type="submit">Salvează</button>
|
||||
<a class="btn ghost" href="{{ url_for('fields.index') }}">Renunță</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card narrow">
|
||||
<h2>Câmpuri disponibile</h2>
|
||||
<p class="muted small">Click pe un câmp pentru a-l insera în text.</p>
|
||||
<div class="field-chips">
|
||||
{% for c in catalog %}
|
||||
<button class="field-chip" type="button" data-insert="{{ c.key }}"
|
||||
title="{{ c.label }} · exemplu: {{ c.sample }}">
|
||||
<span class="chip-label">{{ c.label }}</span>
|
||||
<span class="chip-sample">{{ c.sample }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
var textarea = document.getElementById('template');
|
||||
var preview = document.getElementById('field-preview');
|
||||
var timer = null;
|
||||
var url = {{ url_for('fields.preview')|tojson }};
|
||||
|
||||
function refresh() {
|
||||
var text = textarea.value.trim();
|
||||
if (!text) {
|
||||
preview.textContent = '—';
|
||||
preview.className = 'field-preview muted';
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
fetch(url + '?template=' + encodeURIComponent(text), {
|
||||
headers: { Accept: 'application/json' }
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (!data.ok) {
|
||||
preview.textContent = data.error;
|
||||
preview.className = 'field-preview error';
|
||||
return;
|
||||
}
|
||||
preview.textContent = data.value || '(gol)';
|
||||
preview.className = 'field-preview';
|
||||
})
|
||||
.catch(function () {
|
||||
preview.textContent = 'Previzualizarea nu a putut fi calculată.';
|
||||
preview.className = 'field-preview error';
|
||||
});
|
||||
}, 250);
|
||||
}
|
||||
|
||||
textarea.addEventListener('input', refresh);
|
||||
refresh();
|
||||
|
||||
document.querySelectorAll('[data-insert]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var token = '{{ "{{" }}' + button.dataset.insert + '{{ "}}" }}';
|
||||
var start = textarea.selectionStart || 0;
|
||||
var end = textarea.selectionEnd || 0;
|
||||
textarea.value = textarea.value.slice(0, start) + token + textarea.value.slice(end);
|
||||
textarea.focus();
|
||||
textarea.selectionStart = textarea.selectionEnd = start + token.length;
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,75 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Câmpuri variabile{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<h1>Câmpuri variabile</h1>
|
||||
<p class="subtitle">
|
||||
Text gata de pus pe etichetă, care poate prelua date din pachet, produs sau firmă.
|
||||
</p>
|
||||
</div>
|
||||
<a class="btn primary" href="{{ url_for('fields.create') }}">+ Câmp nou</a>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
{% if fields %}
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Denumire</th>
|
||||
<th>Text</th>
|
||||
<th>Rezultat (demo)</th>
|
||||
<th>Cheie</th>
|
||||
<th>Șabloane</th>
|
||||
<th>Stare</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in fields %}
|
||||
<tr class="{{ 'row-muted' if not f.active }}">
|
||||
<td><strong>{{ f.label }}</strong></td>
|
||||
<td><code class="field-template">{{ f.template }}</code></td>
|
||||
<td>{{ f.preview or "-" }}</td>
|
||||
<td><code class="muted">{{ f.key }}</code></td>
|
||||
<td>
|
||||
{% if f.usage %}<a class="link" href="{{ url_for('label_templates.index') }}">{{ f.usage }}</a>
|
||||
{% else %}<span class="muted">-</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if f.active %}<span class="badge ok">activ</span>
|
||||
{% else %}<span class="badge off">inactiv</span>{% endif %}
|
||||
</td>
|
||||
<td class="right nowrap">
|
||||
<a class="link" href="{{ url_for('fields.edit', field_id=f.id) }}">Editează</a>
|
||||
<form class="inline" method="post" action="{{ url_for('fields.remove', field_id=f.id) }}"
|
||||
onsubmit="return confirm('Ștergi câmpul {{ f.label }}?{% if f.usage %} Este folosit în {{ f.usage }} șabloane și va apărea gol.{% endif %}');">
|
||||
<button class="link danger" type="submit">Șterge</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">
|
||||
Nu există câmpuri proprii. <a href="{{ url_for('fields.create') }}">Adaugă primul câmp</a>
|
||||
sau folosește câmpurile fixe din designer.
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Cum funcționează</h2>
|
||||
<p class="muted small">
|
||||
În textul câmpului poți scrie orice, iar între acolade poți pune un câmp existent:
|
||||
<code>Tel. {{ '{{company.phone}}' }}</code> sau
|
||||
<code>{{ '{{recipe.name}}' }} · lot {{ '{{package.lot}}' }}</code>.
|
||||
La tipărire, acestea sunt înlocuite cu datele reale. Câmpurile proprii apar în designer
|
||||
în grupul <strong>Câmpuri proprii</strong>.
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user