Files
FEDEO/backend/src/utils/functions.ts
florianfederspiel eb718021fd
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 22s
Build and Push Docker Images / build-frontend (push) Successful in 59s
Added Kostenschätzung und Packschein
2026-03-25 15:38:28 +01:00

198 lines
7.2 KiB
TypeScript

import { FastifyInstance } from "fastify"
import { PNG } from "pngjs"
import { Utils } from "@mmote/niimbluelib"
import bwipjs from "bwip-js"
import Sharp from "sharp"
import { tenants } from "../../db/schema"
import { eq } from "drizzle-orm"
export const useNextNumberRangeNumber = async (
server: FastifyInstance,
tenantId: number,
numberRange: string
) => {
const numberRangeFallbacks: Record<string, string> = {
costEstimates: "quotes",
packingSlips: "deliveryNotes",
advanceInvoices: "invoices",
cancellationInvoices: "invoices",
}
const [tenant] = await server.db
.select()
.from(tenants)
.where(eq(tenants.id, tenantId))
if (!tenant) {
throw new Error(`Tenant ${tenantId} not found`)
}
const numberRanges = tenant.numberRanges || {}
const resolvedNumberRange = numberRanges[numberRange]
? numberRange
: numberRangeFallbacks[numberRange]
if (!resolvedNumberRange || !numberRanges[resolvedNumberRange]) {
throw new Error(`Number range '${numberRange}' not found`)
}
const current = numberRanges[resolvedNumberRange]
const usedNumber =
(current.prefix || "") +
current.nextNumber +
(current.suffix || "")
const updatedRanges = {
// @ts-ignore
...numberRanges,
[resolvedNumberRange]: {
...current,
nextNumber: current.nextNumber + 1,
},
}
await server.db
.update(tenants)
.set({ numberRanges: updatedRanges })
.where(eq(tenants.id, tenantId))
return { usedNumber }
}
export async function encodeBase64ToNiimbot(base64Png: string, printDirection: "top" | "left" = "top") {
const buffer = Buffer.from(base64Png, "base64")
const png = PNG.sync.read(buffer)
const { width, height, data } = png
const cols = printDirection === "left" ? height : width
const rows = printDirection === "left" ? width : height
const rowsData: any[] = []
if (cols % 8 !== 0) throw new Error("Column count must be multiple of 8")
for (let row = 0; row < rows; row++) {
let isVoid = true
let blackPixelsCount = 0
const rowData = new Uint8Array(cols / 8)
for (let colOct = 0; colOct < cols / 8; colOct++) {
let pixelsOctet = 0
for (let colBit = 0; colBit < 8; colBit++) {
const x = printDirection === "left" ? row : colOct * 8 + colBit
const y = printDirection === "left" ? height - 1 - (colOct * 8 + colBit) : row
const idx = (y * width + x) * 4
const lum = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]
const isBlack = lum < 128
if (isBlack) {
pixelsOctet |= 1 << (7 - colBit)
isVoid = false
blackPixelsCount++
}
}
rowData[colOct] = pixelsOctet
}
const newPart = {
dataType: isVoid ? "void" : "pixels",
rowNumber: row,
repeat: 1,
rowData: isVoid ? undefined : rowData,
blackPixelsCount,
}
if (rowsData.length === 0) {
rowsData.push(newPart)
} else {
const last = rowsData[rowsData.length - 1]
let same = newPart.dataType === last.dataType
if (same && newPart.dataType === "pixels") {
same = Utils.u8ArraysEqual(newPart.rowData, last.rowData)
}
if (same) last.repeat++
else rowsData.push(newPart)
if (row % 200 === 199) {
rowsData.push({
dataType: "check",
rowNumber: row,
repeat: 0,
rowData: undefined,
blackPixelsCount: 0,
})
}
}
}
return { cols, rows, rowsData }
}
function escapeXml(value: string) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&apos;")
}
export async function generateLabel(context: any = {}, width = 584, height = 354) {
const normalizedWidth = Math.ceil(Number(width) / 8) * 8
const normalizedHeight = Math.max(1, Number(height) || 203)
const idFont = Math.max(24, Math.round(normalizedHeight * 0.125))
const nameFont = Math.max(17, Math.round(normalizedHeight * 0.078))
const customerFont = Math.max(14, Math.round(normalizedHeight * 0.06))
const serialFont = Math.max(12, Math.round(normalizedHeight * 0.052))
const labelId = context.customerInventoryId || context.datamatrix || context.id || "N/A"
const labelName = context.name || context.text || "Kundeninventarartikel"
const customerName = context.customerName || ""
const serial = context.serialNumber ? `SN: ${context.serialNumber}` : ""
const nameLine1 = String(labelName).slice(0, 30)
const nameLine2 = String(labelName).slice(30, 60)
const dataMatrixPng = await bwipjs.toBuffer({
bcid: "datamatrix",
text: String(labelId),
scale: normalizedWidth >= 560 ? 7 : 5,
includetext: false,
})
const dataMatrixMeta = await Sharp(dataMatrixPng).metadata()
const dataMatrixWidth = dataMatrixMeta.width || 0
const dataMatrixHeight = dataMatrixMeta.height || 0
const dmLeft = Math.max(8, normalizedWidth - dataMatrixWidth - 28)
const dmTop = Math.max(8, Math.floor((normalizedHeight - dataMatrixHeight) / 2))
const textMaxWidth = Math.max(120, dmLeft - 20)
const textSvg = `
<svg width="${normalizedWidth}" height="${normalizedHeight}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="white"/>
<text x="12" y="${Math.round(normalizedHeight * 0.15)}" font-size="${idFont}" font-family="Arial, Helvetica, sans-serif" font-weight="700" fill="black">${escapeXml(String(labelId).slice(0, 26))}</text>
<text x="12" y="${Math.round(normalizedHeight * 0.29)}" font-size="${nameFont}" font-family="Arial, Helvetica, sans-serif" fill="black">${escapeXml(nameLine1)}</text>
<text x="12" y="${Math.round(normalizedHeight * 0.37)}" font-size="${nameFont}" font-family="Arial, Helvetica, sans-serif" fill="black">${escapeXml(nameLine2)}</text>
<text x="12" y="${Math.round(normalizedHeight * 0.49)}" font-size="${customerFont}" font-family="Arial, Helvetica, sans-serif" fill="black">${escapeXml(String(customerName).slice(0, 40))}</text>
<text x="12" y="${Math.round(normalizedHeight * 0.58)}" font-size="${serialFont}" font-family="Arial, Helvetica, sans-serif" fill="black">${escapeXml(String(serial).slice(0, 42))}</text>
<rect x="0" y="0" width="${textMaxWidth}" height="${normalizedHeight}" fill="none"/>
</svg>`.trim()
const final = await Sharp({
create: {
width: normalizedWidth,
height: normalizedHeight,
channels: 3,
background: { r: 255, g: 255, b: 255 },
},
})
.composite([
{ input: Buffer.from(textSvg), top: 0, left: 0 },
{ input: dataMatrixPng, top: dmTop, left: dmLeft },
])
.png()
.toBuffer()
return final.toString("base64")
}