u
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import express from 'express';
|
||||
import JSZip from 'jszip';
|
||||
import { renderLabel } from 'react-canvas-label-editor';
|
||||
import * as db from '../../db/article.js';
|
||||
import * as picDb from '../../db/pic.js';
|
||||
import * as printerDb from '../../db/printer.js';
|
||||
import * as labeltemplateDb from '../../db/labeltemplate.js';
|
||||
import { emit } from '../sse.js';
|
||||
import { buildArtikelCsv, toWindows1252 } from '../../util/artikelCsv.js';
|
||||
|
||||
@@ -67,6 +72,22 @@ function articleErrorStatus(error) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
function interpolateString(str, article) {
|
||||
if (typeof str !== 'string') return str;
|
||||
const net = article.net_price ?? article.prices?.[0]?.net;
|
||||
const taxRate = article.prices?.[0]?.taxRate ?? 0;
|
||||
const gross = net != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null;
|
||||
const ean = (article.eans && article.eans.length > 0) ? article.eans[0] : '';
|
||||
|
||||
return str
|
||||
.replace(/\{\{(?:ARTICLE_NAME|article_name|name)\}\}/gi, article.name || '')
|
||||
.replace(/\{\{SKU\}\}/gi, article.sku || '')
|
||||
.replace(/\{\{(?:NET_PRICE|net_price)\}\}/gi, net != null ? net.toString() : '')
|
||||
.replace(/\{\{(?:GROSS_PRICE|gross_price)\}\}/gi, gross != null ? gross.toString() : '')
|
||||
.replace(/\{\{(?:PRICE|price)\}\}/gi, gross != null ? gross.toString() : (net != null ? net.toString() : ''))
|
||||
.replace(/\{\{(?:EAN|ean)\}\}/gi, ean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers article routes (requires auth middleware).
|
||||
* @param {import('express').Application} app - Express app
|
||||
@@ -1502,4 +1523,134 @@ export function register(app, basePath = '') {
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /article/{sku}/print:
|
||||
* post:
|
||||
* tags: [Article]
|
||||
* summary: Print article label
|
||||
* description: Prints a label for the article using a label template.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: sku
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [templateId, printerId, quantity]
|
||||
* properties:
|
||||
* templateId:
|
||||
* type: integer
|
||||
* printerId:
|
||||
* type: integer
|
||||
* quantity:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Print job dispatched
|
||||
* 400:
|
||||
* description: Invalid parameters
|
||||
* 404:
|
||||
* description: Printer, Template or Article not found
|
||||
* 500:
|
||||
* description: Server error
|
||||
*/
|
||||
app.post(`${basePath}/article/:sku/print`, express.json(), async (req, res) => {
|
||||
const companyId = req.user?.selectedCompanyId;
|
||||
if (companyId == null) {
|
||||
return res.status(400).json({ error: 'No company selected' });
|
||||
}
|
||||
|
||||
const { sku: parsedSku, error: skuError } = parseSku(req.params.sku);
|
||||
if (skuError) return res.status(400).json({ error: skuError });
|
||||
const sku = parsedSku;
|
||||
|
||||
const { templateId, printerId, quantity } = req.body;
|
||||
|
||||
const parsedTemplateId = parsePositiveInt(templateId, 'templateId');
|
||||
if (parsedTemplateId.error) return res.status(400).json({ error: parsedTemplateId.error });
|
||||
|
||||
const parsedPrinterId = parsePositiveInt(printerId, 'printerId');
|
||||
if (parsedPrinterId.error) return res.status(400).json({ error: parsedPrinterId.error });
|
||||
|
||||
const parsedQuantity = parsePositiveInt(quantity, 'quantity');
|
||||
if (parsedQuantity.error) return res.status(400).json({ error: parsedQuantity.error });
|
||||
|
||||
try {
|
||||
const printer = await printerDb.getById(req.user.id, parsedPrinterId.id);
|
||||
if (!printer || printer.company_id !== companyId) {
|
||||
return res.status(404).json({ error: 'Printer not found' });
|
||||
}
|
||||
if (printer.type !== 'zpl') {
|
||||
return res.status(400).json({ error: 'Printer is not a ZPL printer' });
|
||||
}
|
||||
|
||||
const template = await labeltemplateDb.getById(req.user.id, parsedTemplateId.id);
|
||||
if (!template || template.company_id !== companyId) {
|
||||
return res.status(404).json({ error: 'Label template not found' });
|
||||
}
|
||||
|
||||
let article;
|
||||
try {
|
||||
const result = await db.getBySku(req.user.id, sku);
|
||||
article = result?.article;
|
||||
} catch (err) {
|
||||
const status = articleErrorStatus(err);
|
||||
if (status === 500) console.error('Get article by sku failed for print:', err);
|
||||
return res.status(status).json({ error: err.message });
|
||||
}
|
||||
if (!article) {
|
||||
return res.status(404).json({ error: 'Article not found' });
|
||||
}
|
||||
|
||||
// Extract and interpolate canvas objects
|
||||
const content = template.content;
|
||||
const objects = Array.isArray(content?.objects)
|
||||
? content.objects
|
||||
: Array.isArray(content)
|
||||
? content
|
||||
: [];
|
||||
|
||||
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: template.width,
|
||||
height: template.height,
|
||||
objects: interpolatedObjects,
|
||||
});
|
||||
|
||||
// Mock network dispatch
|
||||
const printDir = path.join(process.cwd(), 'print_spool', `${printer.address}_${printer.type}`);
|
||||
await fs.mkdir(printDir, { recursive: true });
|
||||
|
||||
for (let i = 0; i < parsedQuantity.id; i++) {
|
||||
const timestamp = Date.now();
|
||||
const filename = parsedQuantity.id > 1 ? `${timestamp}_${i + 1}.png` : `${timestamp}.png`;
|
||||
const filePath = path.join(printDir, filename);
|
||||
await fs.writeFile(filePath, printPayload);
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Print article label failed:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,9 +44,10 @@ export function register(app, basePath = '') {
|
||||
* get:
|
||||
* tags: [LabelTemplate]
|
||||
* summary: List label templates
|
||||
* description: Lists all ZPL label templates for the selected company. The `thumb` field is returned as a base64-encoded PNG string when present.
|
||||
* description: Lists all ZPL label templates for the selected company. The `thumb` field is returned as a base64-encoded PNG string when present. `content.objects` is omitted to reduce payload size.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: labeltemplate/list.md
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of label templates
|
||||
@@ -97,6 +98,7 @@ export function register(app, basePath = '') {
|
||||
* description: Retrieves a single label template for the authenticated user's selected company.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: labeltemplate/get.md
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
@@ -169,6 +171,7 @@ export function register(app, basePath = '') {
|
||||
* and stored with the template.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: labeltemplate/create.md
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
@@ -271,6 +274,7 @@ export function register(app, basePath = '') {
|
||||
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: labeltemplate/update.md
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
@@ -320,6 +324,7 @@ export function register(app, basePath = '') {
|
||||
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: labeltemplate/update.md
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
@@ -374,6 +379,7 @@ export function register(app, basePath = '') {
|
||||
* description: Deletes a label template for the authenticated user's selected company.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: labeltemplate/delete.md
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
|
||||
Reference in New Issue
Block a user