Compare commits
2
Commits
b1c7d89a7d
...
8c5cdf16c4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c5cdf16c4 | ||
|
|
6ba0cdd07f |
No files matched your search
@@ -288,6 +288,27 @@ Drei Wege führen zu einem Bon:
|
|||||||
| **✓ Beenden** | Endbeleg | ist beendet und in der Historie |
|
| **✓ Beenden** | Endbeleg | ist beendet und in der Historie |
|
||||||
| **🖨** in der Historie | Nachdruck desselben Belegs | — |
|
| **🖨** in der Historie | Nachdruck desselben Belegs | — |
|
||||||
|
|
||||||
|
#### Strichcode
|
||||||
|
|
||||||
|
Unter dem Betrag steht ein Strichcode mit dem Artikelnamen, den die Kasse führt
|
||||||
|
(ab Werk `techniker`). Name und An/Aus stehen in den Einstellungen, also in der
|
||||||
|
Datenbank — nicht im Code und nicht in der `.env`.
|
||||||
|
|
||||||
|
Der Code steht ohne Beschriftung und ohne die sonst übliche Klarschrift darunter
|
||||||
|
(`GS H 0`): auf einem Kundenbeleg hat ein interner Artikelname nichts verloren.
|
||||||
|
|
||||||
|
Symbologie ist **CODE128, Zeichensatz B**. CODE39, sonst der Standard für solche
|
||||||
|
Fälle, kennt nur Großbuchstaben und könnte `techniker` gar nicht abbilden.
|
||||||
|
Zeichensatz B deckt den druckbaren ASCII-Bereich ab; alles darüber — Umlaute
|
||||||
|
zumal — weist `PUT /api/settings` ab, statt es still zu verschlucken.
|
||||||
|
|
||||||
|
Die Modulbreite wählt `escpos.js` selbst: so breit, wie die Papierbreite es
|
||||||
|
zulässt (Ruhezonen eingerechnet), denn breitere Balken liest ein Handscanner
|
||||||
|
zuverlässiger. Daraus folgt die Längengrenze — bei 42 Zeichen je Zeile sind es
|
||||||
|
17 Zeichen, bei 32 noch 12. Passt der Name nicht, druckt `receipt.js` ihn nur
|
||||||
|
in Klarschrift: ein abgeschnittener Code würde ohnehin von keinem Scanner
|
||||||
|
gelesen.
|
||||||
|
|
||||||
**Reihenfolge ist Absicht:** Beim Beenden wird erst gespeichert, dann gedruckt.
|
**Reihenfolge ist Absicht:** Beim Beenden wird erst gespeichert, dann gedruckt.
|
||||||
Ein leerer Papierhalter ist damit ein Papierproblem und kein Datenproblem — der
|
Ein leerer Papierhalter ist damit ein Papierproblem und kein Datenproblem — der
|
||||||
Vorgang steht in der Historie und lässt sich von dort nachdrucken. Die
|
Vorgang steht in der Historie und lässt sich von dort nachdrucken. Die
|
||||||
@@ -453,7 +474,7 @@ Alle Endpunkte außer `/health`, `/api/login`, `/api/auth/status` und
|
|||||||
| `POST` | `/api/timers/:id/receipt` | Zwischenbon drucken, Timer bleibt unangetastet |
|
| `POST` | `/api/timers/:id/receipt` | Zwischenbon drucken, Timer bleibt unangetastet |
|
||||||
| `DELETE` | `/api/timers/:id` | verwerfen, **nichts** speichern |
|
| `DELETE` | `/api/timers/:id` | verwerfen, **nichts** speichern |
|
||||||
| `GET` | `/api/settings` | Tarif, Bon-Texte und Druckerzustand lesen |
|
| `GET` | `/api/settings` | Tarif, Bon-Texte und Druckerzustand lesen |
|
||||||
| `PUT` | `/api/settings` | `{rateAmount, rateInterval}` sowie optional `{receiptHeader, receiptFooter, printAutoOnStop}` |
|
| `PUT` | `/api/settings` | `{rateAmount, rateInterval}` sowie optional `{receiptHeader, receiptFooter, printAutoOnStop, receiptBarcodeEnabled, receiptBarcodeArticle}` |
|
||||||
| `GET` | `/api/printer/status` | Erreichbarkeit und Zustand des Druckers |
|
| `GET` | `/api/printer/status` | Erreichbarkeit und Zustand des Druckers |
|
||||||
| `POST` | `/api/printer/test` | Probedruck |
|
| `POST` | `/api/printer/test` | Probedruck |
|
||||||
| `POST` | `/api/printer/provision` | `{host, port, username, password}` → richtet den Pi ein und übernimmt ihn. Antwort enthält das Schrittprotokoll, nie die Zugangsdaten |
|
| `POST` | `/api/printer/provision` | `{host, port, username, password}` → richtet den Pi ein und übernimmt ihn. Antwort enthält das Schrittprotokoll, nie die Zugangsdaten |
|
||||||
|
|||||||
@@ -58,6 +58,23 @@ Zwei Wege, je nachdem, wie der Kunde geht:
|
|||||||
Zettel zeigt den Stand von jetzt und ist als *Zwischenstand* gekennzeichnet;
|
Zettel zeigt den Stand von jetzt und ist als *Zwischenstand* gekennzeichnet;
|
||||||
der Timer läuft unverändert weiter.
|
der Timer läuft unverändert weiter.
|
||||||
|
|
||||||
|
### Strichcode für die Kasse
|
||||||
|
|
||||||
|
Unter dem Betrag steht ein Strichcode. Die Kasse scannt ihn und hat den Artikel
|
||||||
|
sofort auf dem Bildschirm — niemand muss ihn in einer Liste suchen. Der Code
|
||||||
|
steht ohne Beschriftung da: auf einem Kundenbeleg hat ein interner Artikelname
|
||||||
|
nichts verloren.
|
||||||
|
|
||||||
|
Welcher Artikel das ist, steht in den **Einstellungen** unter *Artikel in der
|
||||||
|
Kasse*; ab Werk `techniker`. Wird die Leistung in der Kasse umbenannt, wird der
|
||||||
|
Name hier geändert — ohne dass jemand an die App muss. Der ganze Block lässt
|
||||||
|
sich mit *Strichcode für die Kasse drucken* auch abschalten.
|
||||||
|
|
||||||
|
Zwei Grenzen setzt das Papier: Der Name darf **keine Umlaute und keine
|
||||||
|
Sonderzeichen** enthalten (ein Strichcode kann sie nicht abbilden) und nicht
|
||||||
|
länger sein, als quer auf die Rolle passt — bei 80 mm sind das 17 Zeichen. Beides
|
||||||
|
sagt die App beim Speichern, statt später einen unlesbaren Code zu drucken.
|
||||||
|
|
||||||
### Wenn nichts aus dem Drucker kommt
|
### Wenn nichts aus dem Drucker kommt
|
||||||
|
|
||||||
Der Vorgang ist trotzdem gespeichert — die App sagt in dem Fall ausdrücklich,
|
Der Vorgang ist trotzdem gespeichert — die App sagt in dem Fall ausdrücklich,
|
||||||
@@ -90,6 +107,9 @@ Darunter steht, was auf dem Bon gedruckt wird:
|
|||||||
bezahlen."
|
bezahlen."
|
||||||
* **Bon beim Beenden automatisch drucken** — ausgeschaltet kommt ein Bon nur
|
* **Bon beim Beenden automatisch drucken** — ausgeschaltet kommt ein Bon nur
|
||||||
noch über die 🖨-Knöpfe.
|
noch über die 🖨-Knöpfe.
|
||||||
|
* **Strichcode für die Kasse drucken** — siehe unten. Ausgeschaltet bleibt der
|
||||||
|
Bon genau wie vorher.
|
||||||
|
* **Artikel in der Kasse** — der Name, der im Strichcode steckt.
|
||||||
* **Probedruck** — druckt einen Beispielbon. Beantwortet die Frage, ob der
|
* **Probedruck** — druckt einen Beispielbon. Beantwortet die Frage, ob der
|
||||||
Drucker überhaupt erreichbar ist; darüber steht sein aktueller Zustand.
|
Drucker überhaupt erreichbar ist; darüber steht sein aktueller Zustand.
|
||||||
|
|
||||||
|
|||||||
+65
-1
@@ -20,6 +20,24 @@ const GS = 0x1d;
|
|||||||
// von Node plus diese eine Sonderbehandlung.
|
// von Node plus diese eine Sonderbehandlung.
|
||||||
const CODEPAGE_WPC1252 = 16;
|
const CODEPAGE_WPC1252 = 16;
|
||||||
|
|
||||||
|
// Breite eines Zeichens in Schriftart A, in Druckpunkten. Daraus ergibt sich,
|
||||||
|
// wie viel Platz ein Strichcode quer ueber das Papier hat.
|
||||||
|
const ZEICHENBREITE = 12;
|
||||||
|
|
||||||
|
// Ruhezone links und rechts vom Strichcode, in Modulen. Ohne diesen Abstand
|
||||||
|
// findet ein Scanner den Anfang des Codes nicht zuverlaessig.
|
||||||
|
const RUHEZONE = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wie viele Zeichen ein CODE128-Strichcode auf dieser Papierbreite noch
|
||||||
|
* verkraeftet. Bei der schmalsten noch lesbaren Modulbreite (2 Punkte)
|
||||||
|
* gerechnet, also der guenstigste Fall.
|
||||||
|
*/
|
||||||
|
function barcodeMaxLength(width = 42) {
|
||||||
|
const module = Math.floor((width * ZEICHENBREITE) / 2) - 35 - 2 * RUHEZONE;
|
||||||
|
return Math.max(0, Math.floor(module / 11));
|
||||||
|
}
|
||||||
|
|
||||||
/** Zeichen, die WPC1252 nicht kennt, auf etwas Druckbares abbilden. */
|
/** Zeichen, die WPC1252 nicht kennt, auf etwas Druckbares abbilden. */
|
||||||
const ERSATZ = {
|
const ERSATZ = {
|
||||||
'€': '\x80',
|
'€': '\x80',
|
||||||
@@ -131,6 +149,52 @@ class Bon {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strichcode in CODE128, Zeichensatz B.
|
||||||
|
*
|
||||||
|
* CODE128 statt des sonst ueblichen CODE39, weil CODE39 nur
|
||||||
|
* Grossbuchstaben kennt - ein Artikel namens "techniker" liesse sich damit
|
||||||
|
* nicht abbilden. Zeichensatz B deckt den druckbaren ASCII-Bereich ab und
|
||||||
|
* wird von jedem Handscanner gelesen.
|
||||||
|
*
|
||||||
|
* Die Modulbreite - die Breite des schmalsten Balkens - wird so gross
|
||||||
|
* gewaehlt, wie das Papier es zulaesst. Breitere Balken liest ein Scanner
|
||||||
|
* zuverlaessiger, gerade auf Thermopapier, das schon einen Tag in einer
|
||||||
|
* Hosentasche hinter sich hat.
|
||||||
|
*
|
||||||
|
* @param {string} text Nutzdaten, druckbares ASCII
|
||||||
|
* @param {object} [o]
|
||||||
|
* @param {number} [o.hoehe] Hoehe in Druckpunkten (8 Punkte = 1 mm)
|
||||||
|
* @param {boolean} [o.klarschrift] Nutzdaten zusaetzlich lesbar darunter
|
||||||
|
*/
|
||||||
|
barcode(text, { hoehe = 64, klarschrift = true } = {}) {
|
||||||
|
// Was der Zeichensatz nicht kann, wuerde der Drucker stillschweigend
|
||||||
|
// verschlucken und einen Code ausgeben, der etwas anderes bedeutet.
|
||||||
|
const daten = String(text == null ? '' : text).replace(/[^\x20-\x7e]/g, '');
|
||||||
|
if (!daten) return this;
|
||||||
|
|
||||||
|
// Ein CODE128-Zeichen belegt 11 Module, dazu kommen Start, Pruefzeichen
|
||||||
|
// und Stopp (zusammen 35) sowie die Ruhezone auf beiden Seiten.
|
||||||
|
const module = 11 * daten.length + 35 + 2 * RUHEZONE;
|
||||||
|
const modul = Math.max(2, Math.min(4, Math.floor((this.width * ZEICHENBREITE) / module)));
|
||||||
|
|
||||||
|
this.raw(GS, 0x68, Math.max(1, Math.min(255, hoehe))); // GS h Hoehe
|
||||||
|
this.raw(GS, 0x77, modul); // GS w Modulbreite
|
||||||
|
this.raw(GS, 0x66, 0); // GS f Schrift der Klarschrift
|
||||||
|
this.raw(GS, 0x48, klarschrift ? 2 : 0); // GS H Klarschrift darunter
|
||||||
|
|
||||||
|
// "{B" waehlt den Zeichensatz. Eine geschweifte Klammer in den Daten
|
||||||
|
// muss deshalb verdoppelt werden, sonst liest der Drucker sie als
|
||||||
|
// Umschaltbefehl und der Code enthaelt am Ende etwas anderes.
|
||||||
|
const nutz = Buffer.from('{B' + daten.split('{').join('{{'), 'latin1');
|
||||||
|
|
||||||
|
// Form 2 von GS k: die Laenge steht als Byte davor, statt die Daten mit
|
||||||
|
// einem Nullbyte abzuschliessen. Nur so darf jedes Zeichen vorkommen.
|
||||||
|
this.raw(GS, 0x6b, 73, nutz.length);
|
||||||
|
this.parts.push(nutz);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
feed(n = 1) {
|
feed(n = 1) {
|
||||||
return this.raw(ESC, 0x64, Math.max(0, Math.min(255, n)));
|
return this.raw(ESC, 0x64, Math.max(0, Math.min(255, n)));
|
||||||
}
|
}
|
||||||
@@ -149,4 +213,4 @@ class Bon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Bon, encodeText };
|
module.exports = { Bon, encodeText, barcodeMaxLength };
|
||||||
+54
-2
@@ -10,7 +10,7 @@
|
|||||||
* dem Zielhost gebaut wurde.
|
* dem Zielhost gebaut wurde.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { Bon } = require('./escpos');
|
const { Bon, barcodeMaxLength } = require('./escpos');
|
||||||
|
|
||||||
const p2 = n => String(n).padStart(2, '0');
|
const p2 = n => String(n).padStart(2, '0');
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ function unitCount(ms, rateInterval) {
|
|||||||
* @param {boolean} data.stillRunning
|
* @param {boolean} data.stillRunning
|
||||||
* @param {string} data.header Kopfzeilen, durch \n getrennt
|
* @param {string} data.header Kopfzeilen, durch \n getrennt
|
||||||
* @param {string} data.footer Fusszeilen, durch \n getrennt
|
* @param {string} data.footer Fusszeilen, durch \n getrennt
|
||||||
|
* @param {string} data.barcode Artikelname fuer den Strichcode; leer = keiner
|
||||||
* @param {number} data.width Zeichen je Zeile
|
* @param {number} data.width Zeichen je Zeile
|
||||||
* @returns {Buffer}
|
* @returns {Buffer}
|
||||||
*/
|
*/
|
||||||
@@ -117,6 +118,29 @@ function buildReceipt(data) {
|
|||||||
bon.line();
|
bon.line();
|
||||||
bon.hr();
|
bon.hr();
|
||||||
|
|
||||||
|
// --- Strichcode fuer die Kasse ------------------------------------------
|
||||||
|
// Der Kassierer soll den Artikel nicht in einer Liste suchen muessen: der
|
||||||
|
// Code enthaelt genau den Namen, unter dem die Kasse die Leistung fuehrt.
|
||||||
|
// Bewusst ohne Beschriftung und ohne Klarschrift darunter - der Code steht
|
||||||
|
// fuer sich, und auf einem Kundenbeleg hat ein interner Artikelname nichts
|
||||||
|
// zu suchen.
|
||||||
|
if (data.barcode) {
|
||||||
|
bon.line();
|
||||||
|
bon.align('center');
|
||||||
|
if (data.barcode.length <= barcodeMaxLength(width)) {
|
||||||
|
bon.barcode(data.barcode, { klarschrift: false });
|
||||||
|
} else {
|
||||||
|
// Breiter als das Papier. Ein abgeschnittener Code wird von keinem
|
||||||
|
// Scanner gelesen, also lieber gar keiner: der Name allein reicht
|
||||||
|
// zum Eintippen. Passieren kann das nur, wenn die Papierbreite
|
||||||
|
// nachtraeglich kleiner gestellt wurde.
|
||||||
|
bon.bold(true).line(data.barcode).bold(false);
|
||||||
|
}
|
||||||
|
bon.align('left');
|
||||||
|
bon.line();
|
||||||
|
bon.hr();
|
||||||
|
}
|
||||||
|
|
||||||
// --- Fuss ---------------------------------------------------------------
|
// --- Fuss ---------------------------------------------------------------
|
||||||
bon.align('center');
|
bon.align('center');
|
||||||
if (interim) {
|
if (interim) {
|
||||||
@@ -140,12 +164,40 @@ function buildReceipt(data) {
|
|||||||
return bon.build();
|
return bon.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strichcodes aus der Bytefolge herausloesen und durch ihren Inhalt ersetzen.
|
||||||
|
*
|
||||||
|
* Eigene Schleife statt eines Ausdrucks, weil bei "GS k" die Laenge der Daten
|
||||||
|
* in einem Byte MITTEN in der Sequenz steht - wie weit zu springen ist, weiss
|
||||||
|
* man also erst beim Lesen. Genau das kann ein regulaerer Ausdruck nicht.
|
||||||
|
*/
|
||||||
|
function stripBarcode(s) {
|
||||||
|
let aus = '';
|
||||||
|
let i = 0;
|
||||||
|
while (i < s.length) {
|
||||||
|
if (s.charCodeAt(i) === 0x1d && s[i + 1] === 'k' &&
|
||||||
|
s.charCodeAt(i + 2) >= 65 && s.charCodeAt(i + 2) <= 73) {
|
||||||
|
const laenge = s.charCodeAt(i + 3);
|
||||||
|
const daten = s.substr(i + 4, laenge)
|
||||||
|
.replace(/^\{[ABC]/, '') // Wahl des Zeichensatzes
|
||||||
|
.split('{{').join('{'); // verdoppelte Klammer zuruecknehmen
|
||||||
|
aus += `[Strichcode: ${daten}]\n`;
|
||||||
|
i += 4 + laenge;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
aus += s[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return aus;
|
||||||
|
}
|
||||||
|
|
||||||
/** Bon als reiner Text - fuer Vorschau, Tests und das Journal. */
|
/** Bon als reiner Text - fuer Vorschau, Tests und das Journal. */
|
||||||
function toPlainText(buffer) {
|
function toPlainText(buffer) {
|
||||||
return buffer.toString('latin1')
|
return stripBarcode(buffer.toString('latin1'))
|
||||||
.replace(/\x1b@/g, '') // Zuruecksetzen
|
.replace(/\x1b@/g, '') // Zuruecksetzen
|
||||||
.replace(/\x1b[tadEG!-][\s\S]/g, '') // ESC + Kennbuchstabe + 1 Byte
|
.replace(/\x1b[tadEG!-][\s\S]/g, '') // ESC + Kennbuchstabe + 1 Byte
|
||||||
.replace(/\x1d![\s\S]/g, '') // GS ! Zeichengroesse
|
.replace(/\x1d![\s\S]/g, '') // GS ! Zeichengroesse
|
||||||
|
.replace(/\x1d[hwHf][\s\S]/g, '') // GS h/w/H/f Masse des Strichcodes
|
||||||
.replace(/\x1dV[\s\S]{2}/g, '') // GS V Schnitt
|
.replace(/\x1dV[\s\S]{2}/g, '') // GS V Schnitt
|
||||||
.replace(/\x80/g, 'EUR')
|
.replace(/\x80/g, 'EUR')
|
||||||
.replace(/\n{3,}/g, '\n\n')
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
|||||||
+38
-3
@@ -18,6 +18,7 @@ require('dotenv').config();
|
|||||||
const oidc = require('./oidc');
|
const oidc = require('./oidc');
|
||||||
const printer = require('./printer');
|
const printer = require('./printer');
|
||||||
const { buildReceipt } = require('./receipt');
|
const { buildReceipt } = require('./receipt');
|
||||||
|
const { barcodeMaxLength } = require('./escpos');
|
||||||
const { provision } = require('./provision');
|
const { provision } = require('./provision');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -152,6 +153,12 @@ async function initDatabase() {
|
|||||||
['Bitte an der Kasse bezahlen.\nVielen Dank fuer Ihren Besuch!']);
|
['Bitte an der Kasse bezahlen.\nVielen Dank fuer Ihren Besuch!']);
|
||||||
await dbRun(`INSERT OR IGNORE INTO settings (key,value) VALUES ('print_auto_on_stop','1')`);
|
await dbRun(`INSERT OR IGNORE INTO settings (key,value) VALUES ('print_auto_on_stop','1')`);
|
||||||
|
|
||||||
|
// Strichcode fuer die Kasse: der Kassierer soll den Artikel scannen
|
||||||
|
// koennen, statt ihn zu suchen. Der Name steht hier und nicht im Code,
|
||||||
|
// weil er sich mit dem Artikelstamm der Kasse aendern kann.
|
||||||
|
await dbRun(`INSERT OR IGNORE INTO settings (key,value) VALUES ('receipt_barcode_enabled','1')`);
|
||||||
|
await dbRun(`INSERT OR IGNORE INTO settings (key,value) VALUES ('receipt_barcode_article','techniker')`);
|
||||||
|
|
||||||
// Druckerzugang: die .env liefert nur die ERSTEN Werte. Danach lebt die
|
// Druckerzugang: die .env liefert nur die ERSTEN Werte. Danach lebt die
|
||||||
// Konfiguration in der Datenbank, damit die Oberflaeche einen neuen Pi
|
// Konfiguration in der Datenbank, damit die Oberflaeche einen neuen Pi
|
||||||
// einrichten kann, ohne dass jemand auf den Server muss.
|
// einrichten kann, ohne dass jemand auf den Server muss.
|
||||||
@@ -213,15 +220,19 @@ async function refreshPrinterConfig() {
|
|||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bon-Einstellungen: Kopf-/Fusstext und ob beim Beenden automatisch gedruckt wird. */
|
/** Bon-Einstellungen: Kopf-/Fusstext, Strichcode und der Druck beim Beenden. */
|
||||||
async function getPrintSettings() {
|
async function getPrintSettings() {
|
||||||
const rows = await dbAll(
|
const rows = await dbAll(
|
||||||
`SELECT key, value FROM settings WHERE key IN ('receipt_header','receipt_footer','print_auto_on_stop')`);
|
`SELECT key, value FROM settings WHERE key IN
|
||||||
|
('receipt_header','receipt_footer','print_auto_on_stop',
|
||||||
|
'receipt_barcode_enabled','receipt_barcode_article')`);
|
||||||
const map = Object.fromEntries(rows.map(r => [r.key, r.value]));
|
const map = Object.fromEntries(rows.map(r => [r.key, r.value]));
|
||||||
return {
|
return {
|
||||||
receiptHeader: map.receipt_header || '',
|
receiptHeader: map.receipt_header || '',
|
||||||
receiptFooter: map.receipt_footer || '',
|
receiptFooter: map.receipt_footer || '',
|
||||||
printAutoOnStop: map.print_auto_on_stop !== '0'
|
printAutoOnStop: map.print_auto_on_stop !== '0',
|
||||||
|
receiptBarcodeEnabled: map.receipt_barcode_enabled !== '0',
|
||||||
|
receiptBarcodeArticle: map.receipt_barcode_article || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +288,7 @@ async function printReceipt(vorgang) {
|
|||||||
...vorgang,
|
...vorgang,
|
||||||
header: texte.receiptHeader,
|
header: texte.receiptHeader,
|
||||||
footer: texte.receiptFooter,
|
footer: texte.receiptFooter,
|
||||||
|
barcode: texte.receiptBarcodeEnabled ? texte.receiptBarcodeArticle : '',
|
||||||
width: printer.width()
|
width: printer.width()
|
||||||
});
|
});
|
||||||
const res = await printer.print(daten);
|
const res = await printer.print(daten);
|
||||||
@@ -673,6 +685,29 @@ app.put('/api/settings', requireAuth, wrap(async (req, res) => {
|
|||||||
if (typeof b.printAutoOnStop === 'boolean') {
|
if (typeof b.printAutoOnStop === 'boolean') {
|
||||||
await setSetting('print_auto_on_stop', b.printAutoOnStop ? '1' : '0');
|
await setSetting('print_auto_on_stop', b.printAutoOnStop ? '1' : '0');
|
||||||
}
|
}
|
||||||
|
if (typeof b.receiptBarcodeEnabled === 'boolean') {
|
||||||
|
await setSetting('receipt_barcode_enabled', b.receiptBarcodeEnabled ? '1' : '0');
|
||||||
|
}
|
||||||
|
if (typeof b.receiptBarcodeArticle === 'string') {
|
||||||
|
const artikel = b.receiptBarcodeArticle.trim();
|
||||||
|
// Ein Strichcode kann nur den druckbaren ASCII-Bereich darstellen. Ein
|
||||||
|
// Umlaut wuerde stillschweigend verschwinden - dann scannt die Kasse
|
||||||
|
// spaeter einen Artikel, den es so nicht gibt. Lieber hier sagen, warum.
|
||||||
|
if (/[^\x20-\x7e]/.test(artikel)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Der Artikelname darf keine Umlaute oder Sonderzeichen enthalten - mehr kann ein Strichcode nicht abbilden.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Zu lang heisst: der Code waere breiter als das Papier und damit
|
||||||
|
// abgeschnitten. Ein abgeschnittener Code wird gar nicht erst gelesen.
|
||||||
|
const max = barcodeMaxLength(printer.width());
|
||||||
|
if (artikel.length > max) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Der Artikelname passt nicht auf die Papierbreite: höchstens ${max} Zeichen.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await setSetting('receipt_barcode_article', artikel);
|
||||||
|
}
|
||||||
|
|
||||||
// Druckerzugang von Hand pflegen - fuer einen Pi, der nicht ueber die
|
// Druckerzugang von Hand pflegen - fuer einen Pi, der nicht ueber die
|
||||||
// Einrichtung in dieser Oberflaeche aufgesetzt wurde.
|
// Einrichtung in dieser Oberflaeche aufgesetzt wurde.
|
||||||
|
|||||||
+16
-1
@@ -569,6 +569,9 @@ async function loadSettings() {
|
|||||||
document.getElementById('receiptHeader').value = settings.receiptHeader || '';
|
document.getElementById('receiptHeader').value = settings.receiptHeader || '';
|
||||||
document.getElementById('receiptFooter').value = settings.receiptFooter || '';
|
document.getElementById('receiptFooter').value = settings.receiptFooter || '';
|
||||||
document.getElementById('printAutoOnStop').checked = state.printAutoOnStop;
|
document.getElementById('printAutoOnStop').checked = state.printAutoOnStop;
|
||||||
|
document.getElementById('receiptBarcodeEnabled').checked = settings.receiptBarcodeEnabled !== false;
|
||||||
|
document.getElementById('receiptBarcodeArticle').value = settings.receiptBarcodeArticle || '';
|
||||||
|
toggleBarcodeField();
|
||||||
|
|
||||||
updateSettingsPreview();
|
updateSettingsPreview();
|
||||||
renderPrinterState(settings.printer);
|
renderPrinterState(settings.printer);
|
||||||
@@ -584,11 +587,16 @@ async function saveSettings() {
|
|||||||
const receiptHeader = document.getElementById('receiptHeader').value;
|
const receiptHeader = document.getElementById('receiptHeader').value;
|
||||||
const receiptFooter = document.getElementById('receiptFooter').value;
|
const receiptFooter = document.getElementById('receiptFooter').value;
|
||||||
const printAutoOnStop = document.getElementById('printAutoOnStop').checked;
|
const printAutoOnStop = document.getElementById('printAutoOnStop').checked;
|
||||||
|
const receiptBarcodeEnabled = document.getElementById('receiptBarcodeEnabled').checked;
|
||||||
|
const receiptBarcodeArticle = document.getElementById('receiptBarcodeArticle').value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rate = await api('/api/settings', {
|
const rate = await api('/api/settings', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ rateAmount, rateInterval, receiptHeader, receiptFooter, printAutoOnStop })
|
body: JSON.stringify({
|
||||||
|
rateAmount, rateInterval, receiptHeader, receiptFooter, printAutoOnStop,
|
||||||
|
receiptBarcodeEnabled, receiptBarcodeArticle
|
||||||
|
})
|
||||||
});
|
});
|
||||||
state.rate = rate;
|
state.rate = rate;
|
||||||
state.printAutoOnStop = rate.printAutoOnStop !== false;
|
state.printAutoOnStop = rate.printAutoOnStop !== false;
|
||||||
@@ -600,6 +608,13 @@ async function saveSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Der Artikelname interessiert nur, solange der Code auch gedruckt wird. */
|
||||||
|
function toggleBarcodeField() {
|
||||||
|
const an = document.getElementById('receiptBarcodeEnabled').checked;
|
||||||
|
document.getElementById('barcodeArticleGroup').style.opacity = an ? '1' : '0.45';
|
||||||
|
document.getElementById('receiptBarcodeArticle').disabled = !an;
|
||||||
|
}
|
||||||
|
|
||||||
async function resetSettings() {
|
async function resetSettings() {
|
||||||
if (!confirm('Tarif auf Standard zurücksetzen (€10 pro 10 Minuten)?')) return;
|
if (!confirm('Tarif auf Standard zurücksetzen (€10 pro 10 Minuten)?')) return;
|
||||||
document.getElementById('rateAmount').value = 10;
|
document.getElementById('rateAmount').value = 10;
|
||||||
|
|||||||
+17
-1
@@ -877,6 +877,22 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-row">
|
||||||
|
<input type="checkbox" id="receiptBarcodeEnabled" checked
|
||||||
|
onchange="toggleBarcodeField()">
|
||||||
|
<label for="receiptBarcodeEnabled">Strichcode für die Kasse drucken
|
||||||
|
<small>Die Kasse scannt ihn, statt den Artikel zu suchen.</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="barcodeArticleGroup">
|
||||||
|
<label for="receiptBarcodeArticle">Artikel in der Kasse</label>
|
||||||
|
<input type="text" id="receiptBarcodeArticle" maxlength="32"
|
||||||
|
placeholder="techniker" autocomplete="off" spellcheck="false">
|
||||||
|
<small>Genau der Name, unter dem die Kasse die Leistung führt.
|
||||||
|
Keine Umlaute – der Strichcode kann sie nicht abbilden.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-actions" style="margin-top: 1.25rem;">
|
<div class="settings-actions" style="margin-top: 1.25rem;">
|
||||||
<button type="button" class="btn btn-secondary" id="printerTestBtn"
|
<button type="button" class="btn btn-secondary" id="printerTestBtn"
|
||||||
onclick="testPrint()">🖨️ Probedruck</button>
|
onclick="testPrint()">🖨️ Probedruck</button>
|
||||||
@@ -950,6 +966,6 @@
|
|||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script src="/app.js?v=20260904-emoji"></script>
|
<script src="/app.js?v=20260912-barcode"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user