u
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -29,7 +29,7 @@
|
|||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"pdfkit": "^0.20.2",
|
"pdfkit": "^0.20.2",
|
||||||
"react": "^19.3.1",
|
"react": "^19.3.1",
|
||||||
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor#v19.0.0",
|
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor#v20.0.0",
|
||||||
"selfsigned": "^5.5.0",
|
"selfsigned": "^5.5.0",
|
||||||
"skelehash-wasm": "^0.1.1",
|
"skelehash-wasm": "^0.1.1",
|
||||||
"swagger-ui-dist": "^5.33.3",
|
"swagger-ui-dist": "^5.33.3",
|
||||||
@@ -4379,8 +4379,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-canvas-label-editor": {
|
"node_modules/react-canvas-label-editor": {
|
||||||
"version": "19.0.0",
|
"version": "20.0.0",
|
||||||
"resolved": "git+ssh://git@github.com/sebseb7/react-canvas-label-editor.git#a71e1dfbd1d2d9a0341dfac508b728500222d165",
|
"resolved": "git+ssh://git@github.com/sebseb7/react-canvas-label-editor.git#5dc9a981bbecf5d89861214fe5ff774e073e0c36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"canvaskit-wasm": "^0.42.0",
|
"canvaskit-wasm": "^0.42.0",
|
||||||
"jsbarcode": "^3.12.3",
|
"jsbarcode": "^3.12.3",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"pdfkit": "^0.20.2",
|
"pdfkit": "^0.20.2",
|
||||||
"react": "^19.3.1",
|
"react": "^19.3.1",
|
||||||
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor#v19.0.0",
|
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor#v20.0.0",
|
||||||
"selfsigned": "^5.5.0",
|
"selfsigned": "^5.5.0",
|
||||||
"skelehash-wasm": "^0.1.1",
|
"skelehash-wasm": "^0.1.1",
|
||||||
"swagger-ui-dist": "^5.33.3",
|
"swagger-ui-dist": "^5.33.3",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"swagger-jsdoc": "^6.3.0"
|
"swagger-jsdoc": "^6.3.0"
|
||||||
},
|
},
|
||||||
"allowScripts": {
|
"allowScripts": {
|
||||||
"react-canvas-label-editor@19.0.0": true,
|
"react-canvas-label-editor@20.0.0": true,
|
||||||
"better-sqlite3@13.0.3": true
|
"better-sqlite3@13.0.3": true
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -352,7 +352,27 @@ export async function runSelfTest() {
|
|||||||
apiLog.warn(`WASM image pipeline warning: ${err.message}`);
|
apiLog.warn(`WASM image pipeline warning: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Clean up store connection
|
// 5. Test WASM label rasterizer
|
||||||
|
try {
|
||||||
|
const { renderThumb } = await import('../services/labelThumb.js');
|
||||||
|
const thumb = await renderThumb({
|
||||||
|
width: 300,
|
||||||
|
height: 150,
|
||||||
|
objects: [
|
||||||
|
{ type: 'textbox', text: 'SELFTEST', x: 10, y: 10, font: 'outfit' },
|
||||||
|
{ type: 'barcode', format: 'EAN13', code: '4012345678901', x: 10, y: 40, scale: 2 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (!thumb || thumb.length === 0) {
|
||||||
|
throw new Error('Empty thumbnail rendered');
|
||||||
|
}
|
||||||
|
apiLog.info('WASM label rasterizer: OK');
|
||||||
|
} catch (err) {
|
||||||
|
apiLog.error(`WASM label rasterizer FAILED: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Clean up store connection
|
||||||
try {
|
try {
|
||||||
await closeStore();
|
await closeStore();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { validateObjects } from 'react-canvas-label-editor';
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import { renderLabel, validateObjects } from 'react-canvas-label-editor';
|
||||||
import * as db from '../../db/labeltemplate.js';
|
import * as db from '../../db/labeltemplate.js';
|
||||||
|
import * as printerDb from '../../db/printer.js';
|
||||||
|
import * as agentprinterDb from '../../db/agentprinter.js';
|
||||||
|
import * as printerjobDb from '../../db/printerjob.js';
|
||||||
|
import * as articleDb from '../../db/article.js';
|
||||||
import { renderThumb } from '../../services/labelThumb.js';
|
import { renderThumb } from '../../services/labelThumb.js';
|
||||||
|
import { pngToZpl } from '../../services/zplService.js';
|
||||||
|
import { notifyAgentJob } from '../agent/index.js';
|
||||||
import { emit } from '../sse.js';
|
import { emit } from '../sse.js';
|
||||||
|
import { applyZplCopies } from './zpljob.js';
|
||||||
|
import { interpolateString } from './article/shared.js';
|
||||||
|
|
||||||
// #region Helpers
|
// #region Helpers
|
||||||
|
|
||||||
@@ -482,6 +492,222 @@ export function register(app, basePath = '') {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /labeltemplate/testprint:
|
||||||
|
* post:
|
||||||
|
* tags: [LabelTemplate]
|
||||||
|
* summary: Test print label template
|
||||||
|
* description: |
|
||||||
|
* Dispatches a test print job for a label template or unsaved canvas objects to a specified ZPL printer.
|
||||||
|
* Placeholders in text and barcodes are interpolated using a sample article or the specified article SKU.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* printerId:
|
||||||
|
* type: integer
|
||||||
|
* agentprinterId:
|
||||||
|
* type: integer
|
||||||
|
* templateId:
|
||||||
|
* type: integer
|
||||||
|
* objects:
|
||||||
|
* type: array
|
||||||
|
* width:
|
||||||
|
* type: integer
|
||||||
|
* height:
|
||||||
|
* type: integer
|
||||||
|
* dpi:
|
||||||
|
* type: integer
|
||||||
|
* sku:
|
||||||
|
* type: string
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Print job created
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* 400:
|
||||||
|
* description: Invalid parameters
|
||||||
|
* 404:
|
||||||
|
* description: Printer or template not found
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
*/
|
||||||
|
// #region POST /labeltemplate/testprint
|
||||||
|
|
||||||
|
async function handleTestPrint(req, res) {
|
||||||
|
const selected = requireSelectedCompany(req);
|
||||||
|
if (selected.error) {
|
||||||
|
return res.status(400).json({ error: selected.error });
|
||||||
|
}
|
||||||
|
const companyId = selected.companyId;
|
||||||
|
|
||||||
|
const rawPrinterId = req.body?.printerId ?? req.body?.agentprinterId;
|
||||||
|
const parsedPrinterId = parsePositiveInt(rawPrinterId, 'printerId');
|
||||||
|
if (parsedPrinterId.error) {
|
||||||
|
return res.status(400).json({ error: parsedPrinterId.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let agentPrinter = null;
|
||||||
|
let printer = null;
|
||||||
|
|
||||||
|
if (req.body?.agentprinterId != null) {
|
||||||
|
const parsedApId = parsePositiveInt(req.body.agentprinterId, 'agentprinterId');
|
||||||
|
if (!parsedApId.error) {
|
||||||
|
agentPrinter = await agentprinterDb.getById(req.user.id, parsedApId.id);
|
||||||
|
if (agentPrinter && agentPrinter.company_id === companyId) {
|
||||||
|
printer = agentPrinter;
|
||||||
|
} else {
|
||||||
|
agentPrinter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agentPrinter) {
|
||||||
|
printer = await printerDb.getById(req.user.id, parsedPrinterId.id);
|
||||||
|
if (!printer || printer.company_id !== companyId) {
|
||||||
|
agentPrinter = await agentprinterDb.getById(req.user.id, parsedPrinterId.id);
|
||||||
|
if (agentPrinter && agentPrinter.company_id === companyId) {
|
||||||
|
printer = agentPrinter;
|
||||||
|
} else {
|
||||||
|
return res.status(404).json({ error: 'Printer not found' });
|
||||||
|
}
|
||||||
|
} else if (printer.printeragentId != null) {
|
||||||
|
agentPrinter = printer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (printer.type && printer.type.toLowerCase() !== 'zpl') {
|
||||||
|
return res.status(400).json({ error: 'Printer is not a ZPL printer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawTemplateId = req.params?.id ?? req.body?.templateId;
|
||||||
|
let template = null;
|
||||||
|
if (rawTemplateId != null) {
|
||||||
|
const parsedTmplId = parsePositiveInt(rawTemplateId, 'templateId');
|
||||||
|
if (!parsedTmplId.error) {
|
||||||
|
template = await db.getById(req.user.id, parsedTmplId.id);
|
||||||
|
if (!template || template.company_id !== companyId) {
|
||||||
|
return res.status(404).json({ error: 'Label template not found' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawQuantity = req.body?.quantity ?? 1;
|
||||||
|
const quantity = Number(rawQuantity) > 0 ? Number(rawQuantity) : 1;
|
||||||
|
|
||||||
|
let objects = req.body?.objects;
|
||||||
|
let width = req.body?.width != null ? Number(req.body.width) : template?.width;
|
||||||
|
let height = req.body?.height != null ? Number(req.body.height) : template?.height;
|
||||||
|
let content = req.body?.content ?? template?.content;
|
||||||
|
|
||||||
|
// Resolve sample article for placeholder interpolation
|
||||||
|
let article = null;
|
||||||
|
if (req.body?.sku) {
|
||||||
|
try {
|
||||||
|
const result = await articleDb.getBySku(req.user.id, req.body.sku);
|
||||||
|
article = result?.article;
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!article) {
|
||||||
|
article = {
|
||||||
|
name: req.body?.name || template?.name || 'Musterartikel',
|
||||||
|
sku: 'TEST-12345',
|
||||||
|
prices: [{ net: 19.99, taxRate: 19 }],
|
||||||
|
eans: ['4012345678901'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let zplData;
|
||||||
|
if (typeof content === 'string' && content.includes('^XA')) {
|
||||||
|
let interpolated = interpolateString(content, article);
|
||||||
|
if (quantity > 1) {
|
||||||
|
interpolated = applyZplCopies(interpolated, quantity);
|
||||||
|
}
|
||||||
|
zplData = interpolated;
|
||||||
|
} else {
|
||||||
|
if (!objects && content) {
|
||||||
|
objects = Array.isArray(content?.objects)
|
||||||
|
? content.objects
|
||||||
|
: Array.isArray(content)
|
||||||
|
? content
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(objects)) {
|
||||||
|
objects = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const interpolatedObjects = objects.map((obj) => {
|
||||||
|
const cloned = { ...obj };
|
||||||
|
if (typeof cloned.text === 'string') {
|
||||||
|
cloned.text = interpolateString(cloned.text, article);
|
||||||
|
}
|
||||||
|
if (typeof cloned.code === 'string') {
|
||||||
|
cloned.code = interpolateString(cloned.code, article);
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
});
|
||||||
|
|
||||||
|
const printPayload = await renderLabel({
|
||||||
|
width: width || 448,
|
||||||
|
height: height || 200,
|
||||||
|
objects: interpolatedObjects,
|
||||||
|
});
|
||||||
|
|
||||||
|
zplData = await pngToZpl(printPayload, quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentPrinter) {
|
||||||
|
const job = await printerjobDb.create(req.user.id, {
|
||||||
|
agentprinterId: agentPrinter.id,
|
||||||
|
type: 'zpl',
|
||||||
|
data: zplData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (job.printeragentId) {
|
||||||
|
notifyAgentJob(job.printeragentId, 'created', job);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ success: true, printerjob: job });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for legacy network printers with static IP / address
|
||||||
|
const spoolKey = printer.address ? `${printer.address}_${printer.type}` : `printer_${printer.id}_${printer.type || 'zpl'}`;
|
||||||
|
const printDir = path.join(process.cwd(), 'print_spool', spoolKey);
|
||||||
|
await fs.mkdir(printDir, { recursive: true });
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const filename = `${timestamp}.zpl`;
|
||||||
|
const filePath = path.join(printDir, filename);
|
||||||
|
await fs.writeFile(filePath, zplData, 'utf8');
|
||||||
|
|
||||||
|
return res.status(200).json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Testprint label template failed:', error);
|
||||||
|
return res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post(`${basePath}/labeltemplate/testprint`, handleTestPrint);
|
||||||
|
app.post(`${basePath}/labeltemplate/:id/testprint`, handleTestPrint);
|
||||||
|
|
||||||
|
// #endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user